Replace Timber with Logger

This commit is contained in:
Kirill Kamakin
2022-08-05 20:16:29 +02:00
parent ba5f7322ab
commit 107bb64256
49 changed files with 458 additions and 260 deletions

View File

@@ -102,8 +102,6 @@ dependencies {
implementation(libs.jetbrains.kotlinx.serialization) implementation(libs.jetbrains.kotlinx.serialization)
implementation(libs.jakewharton.timber)
implementation(libs.androidx.paging.runtimeKtx) implementation(libs.androidx.paging.runtimeKtx)
testImplementation(libs.androidx.paging.commonKtx) testImplementation(libs.androidx.paging.commonKtx)

View File

@@ -2,14 +2,17 @@ package gq.kirmanak.mealient
import android.app.Application import android.app.Application
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject
@HiltAndroidApp @HiltAndroidApp
class App : Application() { class App : Application() {
@Inject
lateinit var logger: Logger
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Timber.plant(Timber.DebugTree()) logger.v { "onCreate() called" }
Timber.v("onCreate() called")
} }
} }

View File

@@ -11,9 +11,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet import dagger.multibindings.IntoSet
import gq.kirmanak.mealient.BuildConfig import gq.kirmanak.mealient.BuildConfig
import gq.kirmanak.mealient.logging.Logger
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@@ -22,8 +22,8 @@ object DebugModule {
@Provides @Provides
@Singleton @Singleton
@IntoSet @IntoSet
fun provideLoggingInterceptor(): Interceptor { fun provideLoggingInterceptor(logger: Logger): Interceptor {
val interceptor = HttpLoggingInterceptor { message -> Timber.tag("OkHttp").v(message) } val interceptor = HttpLoggingInterceptor { message -> logger.v(tag = "OkHttp") { message } }
interceptor.level = when { interceptor.level = when {
BuildConfig.LOG_NETWORK -> HttpLoggingInterceptor.Level.BODY BuildConfig.LOG_NETWORK -> HttpLoggingInterceptor.Level.BODY
else -> HttpLoggingInterceptor.Level.BASIC else -> HttpLoggingInterceptor.Level.BASIC

View File

@@ -18,7 +18,9 @@ class AddRecipeDataSourceImpl @Inject constructor(
logger.v { "addRecipe() called with: recipe = $recipe" } logger.v { "addRecipe() called with: recipe = $recipe" }
val service = addRecipeServiceFactory.provideService() val service = addRecipeServiceFactory.provideService()
val response = logAndMapErrors( val response = logAndMapErrors(
block = { service.addRecipe(recipe) }, logProvider = { "addRecipe: can't add recipe" } logger,
block = { service.addRecipe(recipe) },
logProvider = { "addRecipe: can't add recipe" }
) )
logger.v { "addRecipe() response = $response" } logger.v { "addRecipe() response = $response" }
return response return response

View File

@@ -8,10 +8,10 @@ import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
import gq.kirmanak.mealient.data.add.models.AddRecipeSettings import gq.kirmanak.mealient.data.add.models.AddRecipeSettings
import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft
import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -19,6 +19,7 @@ import javax.inject.Singleton
class AddRecipeRepoImpl @Inject constructor( class AddRecipeRepoImpl @Inject constructor(
private val addRecipeDataSource: AddRecipeDataSource, private val addRecipeDataSource: AddRecipeDataSource,
private val addRecipeStorage: AddRecipeStorage, private val addRecipeStorage: AddRecipeStorage,
private val logger: Logger,
) : AddRecipeRepo { ) : AddRecipeRepo {
override val addRecipeRequestFlow: Flow<AddRecipeRequest> override val addRecipeRequestFlow: Flow<AddRecipeRequest>
@@ -37,7 +38,7 @@ class AddRecipeRepoImpl @Inject constructor(
} }
override suspend fun preserve(recipe: AddRecipeRequest) { override suspend fun preserve(recipe: AddRecipeRequest) {
Timber.v("preserveRecipe() called with: recipe = $recipe") logger.v { "preserveRecipe() called with: recipe = $recipe" }
val input = AddRecipeDraft( val input = AddRecipeDraft(
recipeName = recipe.name, recipeName = recipe.name,
recipeDescription = recipe.description, recipeDescription = recipe.description,
@@ -51,12 +52,12 @@ class AddRecipeRepoImpl @Inject constructor(
} }
override suspend fun clear() { override suspend fun clear() {
Timber.v("clear() called") logger.v { "clear() called" }
addRecipeStorage.clear() addRecipeStorage.clear()
} }
override suspend fun saveRecipe(): String { override suspend fun saveRecipe(): String {
Timber.v("saveRecipe() called") logger.v { "saveRecipe() called" }
return addRecipeDataSource.addRecipe(addRecipeRequestFlow.first()) return addRecipeDataSource.addRecipe(addRecipeRequestFlow.first())
} }
} }

View File

@@ -7,10 +7,10 @@ import gq.kirmanak.mealient.data.network.NetworkError.Unauthorized
import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.data.network.ServiceFactory
import gq.kirmanak.mealient.extensions.decodeErrorBodyOrNull import gq.kirmanak.mealient.extensions.decodeErrorBodyOrNull
import gq.kirmanak.mealient.extensions.logAndMapErrors import gq.kirmanak.mealient.extensions.logAndMapErrors
import gq.kirmanak.mealient.logging.Logger
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import retrofit2.HttpException import retrofit2.HttpException
import retrofit2.Response import retrofit2.Response
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -18,14 +18,15 @@ import javax.inject.Singleton
class AuthDataSourceImpl @Inject constructor( class AuthDataSourceImpl @Inject constructor(
private val authServiceFactory: ServiceFactory<AuthService>, private val authServiceFactory: ServiceFactory<AuthService>,
private val json: Json, private val json: Json,
private val logger: Logger,
) : AuthDataSource { ) : AuthDataSource {
override suspend fun authenticate(username: String, password: String): String { override suspend fun authenticate(username: String, password: String): String {
Timber.v("authenticate() called with: username = $username, password = $password") logger.v { "authenticate() called with: username = $username, password = $password" }
val authService = authServiceFactory.provideService() val authService = authServiceFactory.provideService()
val response = sendRequest(authService, username, password) val response = sendRequest(authService, username, password)
val accessToken = parseToken(response) val accessToken = parseToken(response)
Timber.v("authenticate() returned: $accessToken") logger.v { "authenticate() returned: $accessToken" }
return accessToken return accessToken
} }
@@ -34,6 +35,7 @@ class AuthDataSourceImpl @Inject constructor(
username: String, username: String,
password: String password: String
): Response<GetTokenResponse> = logAndMapErrors( ): Response<GetTokenResponse> = logAndMapErrors(
logger,
block = { authService.getToken(username = username, password = password) }, block = { authService.getToken(username = username, password = password) },
logProvider = { "sendRequest: can't get token" }, logProvider = { "sendRequest: can't get token" },
) )
@@ -44,7 +46,7 @@ class AuthDataSourceImpl @Inject constructor(
response.body()?.accessToken ?: throw NotMealie(NullPointerException("Body is null")) response.body()?.accessToken ?: throw NotMealie(NullPointerException("Body is null"))
} else { } else {
val cause = HttpException(response) val cause = HttpException(response)
val errorDetail: ErrorDetail? = response.decodeErrorBodyOrNull(json) val errorDetail: ErrorDetail? = response.decodeErrorBodyOrNull(json, logger)
throw when (errorDetail?.detail) { throw when (errorDetail?.detail) {
"Unauthorized" -> Unauthorized(cause) "Unauthorized" -> Unauthorized(cause)
else -> NotMealie(cause) else -> NotMealie(cause)

View File

@@ -4,9 +4,9 @@ import gq.kirmanak.mealient.data.auth.AuthDataSource
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.auth.AuthStorage import gq.kirmanak.mealient.data.auth.AuthStorage
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -14,13 +14,14 @@ import javax.inject.Singleton
class AuthRepoImpl @Inject constructor( class AuthRepoImpl @Inject constructor(
private val authStorage: AuthStorage, private val authStorage: AuthStorage,
private val authDataSource: AuthDataSource, private val authDataSource: AuthDataSource,
private val logger: Logger,
) : AuthRepo { ) : AuthRepo {
override val isAuthorizedFlow: Flow<Boolean> override val isAuthorizedFlow: Flow<Boolean>
get() = authStorage.authHeaderFlow.map { it != null } get() = authStorage.authHeaderFlow.map { it != null }
override suspend fun authenticate(email: String, password: String) { override suspend fun authenticate(email: String, password: String) {
Timber.v("authenticate() called with: email = $email, password = $password") logger.v { "authenticate() called with: email = $email, password = $password" }
authDataSource.authenticate(email, password) authDataSource.authenticate(email, password)
.let { AUTH_HEADER_FORMAT.format(it) } .let { AUTH_HEADER_FORMAT.format(it) }
.let { authStorage.setAuthHeader(it) } .let { authStorage.setAuthHeader(it) }
@@ -35,14 +36,14 @@ class AuthRepoImpl @Inject constructor(
} }
override suspend fun logout() { override suspend fun logout() {
Timber.v("logout() called") logger.v { "logout() called" }
authStorage.setEmail(null) authStorage.setEmail(null)
authStorage.setPassword(null) authStorage.setPassword(null)
authStorage.setAuthHeader(null) authStorage.setAuthHeader(null)
} }
override suspend fun invalidateAuthHeader() { override suspend fun invalidateAuthHeader() {
Timber.v("invalidateAuthHeader() called") logger.v { "invalidateAuthHeader() called" }
val email = authStorage.getEmail() ?: return val email = authStorage.getEmail() ?: return
val password = authStorage.getPassword() ?: return val password = authStorage.getPassword() ?: return
runCatchingExceptCancel { authenticate(email, password) } runCatchingExceptCancel { authenticate(email, password) }

View File

@@ -6,11 +6,11 @@ import androidx.core.content.edit
import gq.kirmanak.mealient.data.auth.AuthStorage import gq.kirmanak.mealient.data.auth.AuthStorage
import gq.kirmanak.mealient.datastore.DataStoreModule.Companion.ENCRYPTED import gq.kirmanak.mealient.datastore.DataStoreModule.Companion.ENCRYPTED
import gq.kirmanak.mealient.extensions.prefsChangeFlow import gq.kirmanak.mealient.extensions.prefsChangeFlow
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.Executors import java.util.concurrent.Executors
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Named import javax.inject.Named
@@ -19,11 +19,12 @@ import javax.inject.Singleton
@Singleton @Singleton
class AuthStorageImpl @Inject constructor( class AuthStorageImpl @Inject constructor(
@Named(ENCRYPTED) private val sharedPreferences: SharedPreferences, @Named(ENCRYPTED) private val sharedPreferences: SharedPreferences,
private val logger: Logger,
) : AuthStorage { ) : AuthStorage {
override val authHeaderFlow: Flow<String?> override val authHeaderFlow: Flow<String?>
get() = sharedPreferences get() = sharedPreferences
.prefsChangeFlow { getString(AUTH_HEADER_KEY, null) } .prefsChangeFlow(logger) { getString(AUTH_HEADER_KEY, null) }
.distinctUntilChanged() .distinctUntilChanged()
private val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
@@ -43,13 +44,13 @@ class AuthStorageImpl @Inject constructor(
key: String, key: String,
value: String? value: String?
) = withContext(singleThreadDispatcher) { ) = withContext(singleThreadDispatcher) {
Timber.v("putString() called with: key = $key, value = $value") logger.v { "putString() called with: key = $key, value = $value" }
sharedPreferences.edit(commit = true) { putString(key, value) } sharedPreferences.edit(commit = true) { putString(key, value) }
} }
private suspend fun getString(key: String) = withContext(singleThreadDispatcher) { private suspend fun getString(key: String) = withContext(singleThreadDispatcher) {
val result = sharedPreferences.getString(key, null) val result = sharedPreferences.getString(key, null)
Timber.v("getString() called with: key = $key, returned: $result") logger.v { "getString() called with: key = $key, returned: $result" }
result result
} }

View File

@@ -5,20 +5,22 @@ import gq.kirmanak.mealient.data.baseurl.VersionInfo
import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.data.network.ServiceFactory
import gq.kirmanak.mealient.extensions.logAndMapErrors import gq.kirmanak.mealient.extensions.logAndMapErrors
import gq.kirmanak.mealient.extensions.versionInfo import gq.kirmanak.mealient.extensions.versionInfo
import timber.log.Timber import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class VersionDataSourceImpl @Inject constructor( class VersionDataSourceImpl @Inject constructor(
private val serviceFactory: ServiceFactory<VersionService>, private val serviceFactory: ServiceFactory<VersionService>,
private val logger: Logger,
) : VersionDataSource { ) : VersionDataSource {
override suspend fun getVersionInfo(baseUrl: String): VersionInfo { override suspend fun getVersionInfo(baseUrl: String): VersionInfo {
Timber.v("getVersionInfo() called with: baseUrl = $baseUrl") logger.v { "getVersionInfo() called with: baseUrl = $baseUrl" }
val service = serviceFactory.provideService(baseUrl) val service = serviceFactory.provideService(baseUrl)
val response = logAndMapErrors( val response = logAndMapErrors(
logger,
block = { service.getVersion() }, block = { service.getVersion() },
logProvider = { "getVersionInfo: can't request version" } logProvider = { "getVersionInfo: can't request version" }
) )

View File

@@ -2,15 +2,16 @@ package gq.kirmanak.mealient.data.disclaimer
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import gq.kirmanak.mealient.data.storage.PreferencesStorage import gq.kirmanak.mealient.data.storage.PreferencesStorage
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class DisclaimerStorageImpl @Inject constructor( class DisclaimerStorageImpl @Inject constructor(
private val preferencesStorage: PreferencesStorage, private val preferencesStorage: PreferencesStorage,
private val logger: Logger,
) : DisclaimerStorage { ) : DisclaimerStorage {
private val isDisclaimerAcceptedKey: Preferences.Key<Boolean> private val isDisclaimerAcceptedKey: Preferences.Key<Boolean>
@@ -19,14 +20,14 @@ class DisclaimerStorageImpl @Inject constructor(
get() = preferencesStorage.valueUpdates(isDisclaimerAcceptedKey).map { it == true } get() = preferencesStorage.valueUpdates(isDisclaimerAcceptedKey).map { it == true }
override suspend fun isDisclaimerAccepted(): Boolean { override suspend fun isDisclaimerAccepted(): Boolean {
Timber.v("isDisclaimerAccepted() called") logger.v { "isDisclaimerAccepted() called" }
val isAccepted = preferencesStorage.getValue(isDisclaimerAcceptedKey) ?: false val isAccepted = preferencesStorage.getValue(isDisclaimerAcceptedKey) ?: false
Timber.v("isDisclaimerAccepted() returned: $isAccepted") logger.v { "isDisclaimerAccepted() returned: $isAccepted" }
return isAccepted return isAccepted
} }
override suspend fun acceptDisclaimer() { override suspend fun acceptDisclaimer() {
Timber.v("acceptDisclaimer() called") logger.v { "acceptDisclaimer() called" }
preferencesStorage.storeValues(Pair(isDisclaimerAcceptedKey, true)) preferencesStorage.storeValues(Pair(isDisclaimerAcceptedKey, true))
} }
} }

View File

@@ -1,21 +1,22 @@
package gq.kirmanak.mealient.data.network package gq.kirmanak.mealient.data.network
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import gq.kirmanak.mealient.logging.Logger
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import retrofit2.Retrofit import retrofit2.Retrofit
import timber.log.Timber
class RetrofitBuilder( class RetrofitBuilder(
private val okHttpClient: OkHttpClient, private val okHttpClient: OkHttpClient,
private val json: Json private val json: Json,
private val logger: Logger,
) { ) {
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
fun buildRetrofit(baseUrl: String): Retrofit { fun buildRetrofit(baseUrl: String): Retrofit {
Timber.v("buildRetrofit() called with: baseUrl = $baseUrl") logger.v { "buildRetrofit() called with: baseUrl = $baseUrl" }
val contentType = "application/json".toMediaType() val contentType = "application/json".toMediaType()
val converterFactory = json.asConverterFactory(contentType) val converterFactory = json.asConverterFactory(contentType)
return Retrofit.Builder() return Retrofit.Builder()

View File

@@ -2,30 +2,34 @@ package gq.kirmanak.mealient.data.network
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import timber.log.Timber import gq.kirmanak.mealient.logging.Logger
inline fun <reified T> RetrofitBuilder.createServiceFactory(baseURLStorage: BaseURLStorage) = inline fun <reified T> RetrofitBuilder.createServiceFactory(
RetrofitServiceFactory(T::class.java, this, baseURLStorage) baseURLStorage: BaseURLStorage,
logger: Logger
) =
RetrofitServiceFactory(T::class.java, this, baseURLStorage, logger)
class RetrofitServiceFactory<T>( class RetrofitServiceFactory<T>(
private val serviceClass: Class<T>, private val serviceClass: Class<T>,
private val retrofitBuilder: RetrofitBuilder, private val retrofitBuilder: RetrofitBuilder,
private val baseURLStorage: BaseURLStorage, private val baseURLStorage: BaseURLStorage,
private val logger: Logger,
) : ServiceFactory<T> { ) : ServiceFactory<T> {
private val cache: MutableMap<String, T> = mutableMapOf() private val cache: MutableMap<String, T> = mutableMapOf()
override suspend fun provideService(baseUrl: String?): T = runCatchingExceptCancel { override suspend fun provideService(baseUrl: String?): T = runCatchingExceptCancel {
Timber.v("provideService() called with: baseUrl = $baseUrl, class = ${serviceClass.simpleName}") logger.v { "provideService() called with: baseUrl = $baseUrl, class = ${serviceClass.simpleName}" }
val url = baseUrl ?: baseURLStorage.requireBaseURL() val url = baseUrl ?: baseURLStorage.requireBaseURL()
synchronized(cache) { cache[url] ?: createService(url, serviceClass) } synchronized(cache) { cache[url] ?: createService(url, serviceClass) }
}.getOrElse { }.getOrElse {
Timber.e(it, "provideService: can't provide service for $baseUrl") logger.e(it) { "provideService: can't provide service for $baseUrl" }
throw NetworkError.MalformedUrl(it) throw NetworkError.MalformedUrl(it)
} }
private fun createService(url: String, serviceClass: Class<T>): T { private fun createService(url: String, serviceClass: Class<T>): T {
Timber.v("createService() called with: url = $url, serviceClass = ${serviceClass.simpleName}") logger.v { "createService() called with: url = $url, serviceClass = ${serviceClass.simpleName}" }
val service = retrofitBuilder.buildRetrofit(url).create(serviceClass) val service = retrofitBuilder.buildRetrofit(url).create(serviceClass)
cache[url] = service cache[url] = service
return service return service

View File

@@ -11,20 +11,21 @@ import gq.kirmanak.mealient.extensions.recipeEntity
import gq.kirmanak.mealient.extensions.toRecipeEntity import gq.kirmanak.mealient.extensions.toRecipeEntity
import gq.kirmanak.mealient.extensions.toRecipeIngredientEntity import gq.kirmanak.mealient.extensions.toRecipeIngredientEntity
import gq.kirmanak.mealient.extensions.toRecipeInstructionEntity import gq.kirmanak.mealient.extensions.toRecipeInstructionEntity
import timber.log.Timber import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class RecipeStorageImpl @Inject constructor( class RecipeStorageImpl @Inject constructor(
private val db: AppDb private val db: AppDb,
private val logger: Logger,
) : RecipeStorage { ) : RecipeStorage {
private val recipeDao: RecipeDao by lazy { db.recipeDao() } private val recipeDao: RecipeDao by lazy { db.recipeDao() }
override suspend fun saveRecipes( override suspend fun saveRecipes(
recipes: List<GetRecipeSummaryResponse> recipes: List<GetRecipeSummaryResponse>
) = db.withTransaction { ) = db.withTransaction {
Timber.v("saveRecipes() called with $recipes") logger.v { "saveRecipes() called with $recipes" }
val tagEntities = mutableSetOf<TagEntity>() val tagEntities = mutableSetOf<TagEntity>()
tagEntities.addAll(recipeDao.queryAllTags()) tagEntities.addAll(recipeDao.queryAllTags())
@@ -91,12 +92,12 @@ class RecipeStorageImpl @Inject constructor(
override fun queryRecipes(): PagingSource<Int, RecipeSummaryEntity> { override fun queryRecipes(): PagingSource<Int, RecipeSummaryEntity> {
Timber.v("queryRecipes() called") logger.v { "queryRecipes() called" }
return recipeDao.queryRecipesByPages() return recipeDao.queryRecipesByPages()
} }
override suspend fun refreshAll(recipes: List<GetRecipeSummaryResponse>) { override suspend fun refreshAll(recipes: List<GetRecipeSummaryResponse>) {
Timber.v("refreshAll() called with: recipes = $recipes") logger.v { "refreshAll() called with: recipes = $recipes" }
db.withTransaction { db.withTransaction {
recipeDao.removeAllRecipes() recipeDao.removeAllRecipes()
saveRecipes(recipes) saveRecipes(recipes)
@@ -104,7 +105,7 @@ class RecipeStorageImpl @Inject constructor(
} }
override suspend fun clearAllLocalData() { override suspend fun clearAllLocalData() {
Timber.v("clearAllLocalData() called") logger.v { "clearAllLocalData() called" }
db.withTransaction { db.withTransaction {
recipeDao.removeAllRecipes() recipeDao.removeAllRecipes()
recipeDao.removeAllCategories() recipeDao.removeAllCategories()
@@ -113,7 +114,7 @@ class RecipeStorageImpl @Inject constructor(
} }
override suspend fun saveRecipeInfo(recipe: GetRecipeResponse) { override suspend fun saveRecipeInfo(recipe: GetRecipeResponse) {
Timber.v("saveRecipeInfo() called with: recipe = $recipe") logger.v { "saveRecipeInfo() called with: recipe = $recipe" }
db.withTransaction { db.withTransaction {
recipeDao.insertRecipe(recipe.toRecipeEntity()) recipeDao.insertRecipe(recipe.toRecipeEntity())
@@ -132,11 +133,11 @@ class RecipeStorageImpl @Inject constructor(
} }
override suspend fun queryRecipeInfo(recipeId: Long): FullRecipeInfo { override suspend fun queryRecipeInfo(recipeId: Long): FullRecipeInfo {
Timber.v("queryRecipeInfo() called with: recipeId = $recipeId") logger.v { "queryRecipeInfo() called with: recipeId = $recipeId" }
val fullRecipeInfo = checkNotNull(recipeDao.queryFullRecipeInfo(recipeId)) { val fullRecipeInfo = checkNotNull(recipeDao.queryFullRecipeInfo(recipeId)) {
"Can't find recipe by id $recipeId in DB" "Can't find recipe by id $recipeId in DB"
} }
Timber.v("queryRecipeInfo() returned: $fullRecipeInfo") logger.v { "queryRecipeInfo() returned: $fullRecipeInfo" }
return fullRecipeInfo return fullRecipeInfo
} }
} }

View File

@@ -1,18 +1,19 @@
package gq.kirmanak.mealient.data.recipes.impl package gq.kirmanak.mealient.data.recipes.impl
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
import gq.kirmanak.mealient.logging.Logger
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class RecipeImageUrlProviderImpl @Inject constructor( class RecipeImageUrlProviderImpl @Inject constructor(
private val baseURLStorage: BaseURLStorage, private val baseURLStorage: BaseURLStorage,
private val logger: Logger,
) : RecipeImageUrlProvider { ) : RecipeImageUrlProvider {
override suspend fun generateImageUrl(slug: String?): String? { override suspend fun generateImageUrl(slug: String?): String? {
Timber.v("generateImageUrl() called with: slug = $slug") logger.v { "generateImageUrl() called with: slug = $slug" }
slug?.takeUnless { it.isBlank() } ?: return null slug?.takeUnless { it.isBlank() } ?: return null
val imagePath = IMAGE_PATH_FORMAT.format(slug) val imagePath = IMAGE_PATH_FORMAT.format(slug)
val baseUrl = baseURLStorage.getBaseURL()?.takeUnless { it.isEmpty() } val baseUrl = baseURLStorage.getBaseURL()?.takeUnless { it.isEmpty() }
@@ -21,7 +22,7 @@ class RecipeImageUrlProviderImpl @Inject constructor(
?.addPathSegments(imagePath) ?.addPathSegments(imagePath)
?.build() ?.build()
?.toString() ?.toString()
Timber.v("getRecipeImageUrl() returned: $result") logger.v { "getRecipeImageUrl() returned: $result" }
return result return result
} }

View File

@@ -10,7 +10,7 @@ import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.database.recipe.entity.FullRecipeInfo import gq.kirmanak.mealient.database.recipe.entity.FullRecipeInfo
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import timber.log.Timber import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -21,9 +21,10 @@ class RecipeRepoImpl @Inject constructor(
private val storage: RecipeStorage, private val storage: RecipeStorage,
private val pagingSourceFactory: InvalidatingPagingSourceFactory<Int, RecipeSummaryEntity>, private val pagingSourceFactory: InvalidatingPagingSourceFactory<Int, RecipeSummaryEntity>,
private val dataSource: RecipeDataSource, private val dataSource: RecipeDataSource,
private val logger: Logger,
) : RecipeRepo { ) : RecipeRepo {
override fun createPager(): Pager<Int, RecipeSummaryEntity> { override fun createPager(): Pager<Int, RecipeSummaryEntity> {
Timber.v("createPager() called") logger.v { "createPager() called" }
val pagingConfig = PagingConfig(pageSize = 5, enablePlaceholders = true) val pagingConfig = PagingConfig(pageSize = 5, enablePlaceholders = true)
return Pager( return Pager(
config = pagingConfig, config = pagingConfig,
@@ -33,17 +34,17 @@ class RecipeRepoImpl @Inject constructor(
} }
override suspend fun clearLocalData() { override suspend fun clearLocalData() {
Timber.v("clearLocalData() called") logger.v { "clearLocalData() called" }
storage.clearAllLocalData() storage.clearAllLocalData()
} }
override suspend fun loadRecipeInfo(recipeId: Long, recipeSlug: String): FullRecipeInfo { override suspend fun loadRecipeInfo(recipeId: Long, recipeSlug: String): FullRecipeInfo {
Timber.v("loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug") logger.v { "loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug" }
runCatchingExceptCancel { runCatchingExceptCancel {
storage.saveRecipeInfo(dataSource.requestRecipeInfo(recipeSlug)) storage.saveRecipeInfo(dataSource.requestRecipeInfo(recipeSlug))
}.onFailure { }.onFailure {
Timber.e(it, "loadRecipeInfo: can't update full recipe info") logger.e(it) { "loadRecipeInfo: can't update full recipe info" }
} }
return storage.queryRecipeInfo(recipeId) return storage.queryRecipeInfo(recipeId)

View File

@@ -8,7 +8,7 @@ import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import timber.log.Timber import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -18,6 +18,7 @@ class RecipesRemoteMediator @Inject constructor(
private val storage: RecipeStorage, private val storage: RecipeStorage,
private val network: RecipeDataSource, private val network: RecipeDataSource,
private val pagingSourceFactory: InvalidatingPagingSourceFactory<Int, RecipeSummaryEntity>, private val pagingSourceFactory: InvalidatingPagingSourceFactory<Int, RecipeSummaryEntity>,
private val logger: Logger,
) : RemoteMediator<Int, RecipeSummaryEntity>() { ) : RemoteMediator<Int, RecipeSummaryEntity>() {
@VisibleForTesting @VisibleForTesting
@@ -27,10 +28,10 @@ class RecipesRemoteMediator @Inject constructor(
loadType: LoadType, loadType: LoadType,
state: PagingState<Int, RecipeSummaryEntity> state: PagingState<Int, RecipeSummaryEntity>
): MediatorResult { ): MediatorResult {
Timber.v("load() called with: lastRequestEnd = $lastRequestEnd, loadType = $loadType, state = $state") logger.v { "load() called with: lastRequestEnd = $lastRequestEnd, loadType = $loadType, state = $state" }
if (loadType == PREPEND) { if (loadType == PREPEND) {
Timber.i("load: early exit, PREPEND isn't supported") logger.i { "load: early exit, PREPEND isn't supported" }
return MediatorResult.Success(endOfPaginationReached = true) return MediatorResult.Success(endOfPaginationReached = true)
} }
@@ -43,7 +44,7 @@ class RecipesRemoteMediator @Inject constructor(
else storage.saveRecipes(recipes) else storage.saveRecipes(recipes)
recipes.size recipes.size
}.getOrElse { }.getOrElse {
Timber.e(it, "load: can't load recipes") logger.e(it) { "load: can't load recipes" }
return MediatorResult.Error(it) return MediatorResult.Error(it)
} }
@@ -53,7 +54,7 @@ class RecipesRemoteMediator @Inject constructor(
// Read that trick here https://github.com/android/architecture-components-samples/issues/889#issuecomment-880847858 // Read that trick here https://github.com/android/architecture-components-samples/issues/889#issuecomment-880847858
pagingSourceFactory.invalidate() pagingSourceFactory.invalidate()
Timber.d("load: expectedCount = $limit, received $count") logger.d { "load: expectedCount = $limit, received $count" }
lastRequestEnd = start + count lastRequestEnd = start + count
return MediatorResult.Success(endOfPaginationReached = count < limit) return MediatorResult.Success(endOfPaginationReached = count < limit)
} }

View File

@@ -3,31 +3,32 @@ package gq.kirmanak.mealient.data.recipes.network
import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.data.network.ServiceFactory
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
import timber.log.Timber import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class RecipeDataSourceImpl @Inject constructor( class RecipeDataSourceImpl @Inject constructor(
private val recipeServiceFactory: ServiceFactory<RecipeService>, private val recipeServiceFactory: ServiceFactory<RecipeService>,
private val logger: Logger,
) : RecipeDataSource { ) : RecipeDataSource {
override suspend fun requestRecipes(start: Int, limit: Int): List<GetRecipeSummaryResponse> { override suspend fun requestRecipes(start: Int, limit: Int): List<GetRecipeSummaryResponse> {
Timber.v("requestRecipes() called with: start = $start, limit = $limit") logger.v { "requestRecipes() called with: start = $start, limit = $limit" }
val recipeSummary = getRecipeService().getRecipeSummary(start, limit) val recipeSummary = getRecipeService().getRecipeSummary(start, limit)
Timber.v("requestRecipes() returned: $recipeSummary") logger.v { "requestRecipes() returned: $recipeSummary" }
return recipeSummary return recipeSummary
} }
override suspend fun requestRecipeInfo(slug: String): GetRecipeResponse { override suspend fun requestRecipeInfo(slug: String): GetRecipeResponse {
Timber.v("requestRecipeInfo() called with: slug = $slug") logger.v { "requestRecipeInfo() called with: slug = $slug" }
val recipeInfo = getRecipeService().getRecipe(slug) val recipeInfo = getRecipeService().getRecipe(slug)
Timber.v("requestRecipeInfo() returned: $recipeInfo") logger.v { "requestRecipeInfo() returned: $recipeInfo" }
return recipeInfo return recipeInfo
} }
private suspend fun getRecipeService(): RecipeService { private suspend fun getRecipeService(): RecipeService {
Timber.v("getRecipeService() called") logger.v { "getRecipeService() called" }
return recipeServiceFactory.provideService() return recipeServiceFactory.provideService()
} }
} }

View File

@@ -5,14 +5,15 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class PreferencesStorageImpl @Inject constructor( class PreferencesStorageImpl @Inject constructor(
private val dataStore: DataStore<Preferences> private val dataStore: DataStore<Preferences>,
private val logger: Logger,
) : PreferencesStorage { ) : PreferencesStorage {
override val baseUrlKey = stringPreferencesKey("baseUrl") override val baseUrlKey = stringPreferencesKey("baseUrl")
@@ -21,7 +22,7 @@ class PreferencesStorageImpl @Inject constructor(
override suspend fun <T> getValue(key: Preferences.Key<T>): T? { override suspend fun <T> getValue(key: Preferences.Key<T>): T? {
val value = dataStore.data.first()[key] val value = dataStore.data.first()[key]
Timber.v("getValue() returned: $value for $key") logger.v { "getValue() returned: $value for $key" }
return value return value
} }
@@ -29,23 +30,23 @@ class PreferencesStorageImpl @Inject constructor(
checkNotNull(getValue(key)) { "Value at $key is null when it was required" } checkNotNull(getValue(key)) { "Value at $key is null when it was required" }
override suspend fun <T> storeValues(vararg pairs: Pair<Preferences.Key<T>, T>) { override suspend fun <T> storeValues(vararg pairs: Pair<Preferences.Key<T>, T>) {
Timber.v("storeValues() called with: pairs = ${pairs.contentToString()}") logger.v { "storeValues() called with: pairs = ${pairs.contentToString()}" }
dataStore.edit { preferences -> dataStore.edit { preferences ->
pairs.forEach { preferences += it.toPreferencesPair() } pairs.forEach { preferences += it.toPreferencesPair() }
} }
} }
override fun <T> valueUpdates(key: Preferences.Key<T>): Flow<T?> { override fun <T> valueUpdates(key: Preferences.Key<T>): Flow<T?> {
Timber.v("valueUpdates() called with: key = $key") logger.v { "valueUpdates() called with: key = $key" }
return dataStore.data return dataStore.data
.map { it[key] } .map { it[key] }
.distinctUntilChanged() .distinctUntilChanged()
.onEach { Timber.d("valueUpdates: new value at $key is $it") } .onEach { logger.d { "valueUpdates: new value at $key is $it" } }
.onCompletion { Timber.i(it, "valueUpdates: finished") } .onCompletion { logger.i(it) { "valueUpdates: finished" } }
} }
override suspend fun <T> removeValues(vararg keys: Preferences.Key<T>) { override suspend fun <T> removeValues(vararg keys: Preferences.Key<T>) {
Timber.v("removeValues() called with: key = ${keys.contentToString()}") logger.v { "removeValues() called with: key = ${keys.contentToString()}" }
dataStore.edit { preferences -> dataStore.edit { preferences ->
keys.forEach { preferences -= it } keys.forEach { preferences -= it }
} }

View File

@@ -16,6 +16,7 @@ import gq.kirmanak.mealient.data.network.ServiceFactory
import gq.kirmanak.mealient.data.network.createServiceFactory import gq.kirmanak.mealient.data.network.createServiceFactory
import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage
import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorageImpl import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorageImpl
import gq.kirmanak.mealient.logging.Logger
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import javax.inject.Named import javax.inject.Named
@@ -32,9 +33,13 @@ interface AddRecipeModule {
fun provideAddRecipeServiceFactory( fun provideAddRecipeServiceFactory(
@Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient, @Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient,
json: Json, json: Json,
logger: Logger,
baseURLStorage: BaseURLStorage, baseURLStorage: BaseURLStorage,
): ServiceFactory<AddRecipeService> { ): ServiceFactory<AddRecipeService> {
return RetrofitBuilder(okHttpClient, json).createServiceFactory(baseURLStorage) return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory(
baseURLStorage,
logger
)
} }
} }

View File

@@ -19,6 +19,7 @@ import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
import gq.kirmanak.mealient.data.network.RetrofitBuilder import gq.kirmanak.mealient.data.network.RetrofitBuilder
import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.data.network.ServiceFactory
import gq.kirmanak.mealient.data.network.createServiceFactory import gq.kirmanak.mealient.data.network.createServiceFactory
import gq.kirmanak.mealient.logging.Logger
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import javax.inject.Named import javax.inject.Named
@@ -35,9 +36,13 @@ interface AuthModule {
fun provideAuthServiceFactory( fun provideAuthServiceFactory(
@Named(NO_AUTH_OK_HTTP) okHttpClient: OkHttpClient, @Named(NO_AUTH_OK_HTTP) okHttpClient: OkHttpClient,
json: Json, json: Json,
logger: Logger,
baseURLStorage: BaseURLStorage, baseURLStorage: BaseURLStorage,
): ServiceFactory<AuthService> { ): ServiceFactory<AuthService> {
return RetrofitBuilder(okHttpClient, json).createServiceFactory(baseURLStorage) return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory(
baseURLStorage,
logger
)
} }
@Provides @Provides

View File

@@ -13,6 +13,7 @@ import gq.kirmanak.mealient.data.baseurl.impl.VersionService
import gq.kirmanak.mealient.data.network.RetrofitBuilder import gq.kirmanak.mealient.data.network.RetrofitBuilder
import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.data.network.ServiceFactory
import gq.kirmanak.mealient.data.network.createServiceFactory import gq.kirmanak.mealient.data.network.createServiceFactory
import gq.kirmanak.mealient.logging.Logger
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import javax.inject.Named import javax.inject.Named
@@ -29,9 +30,13 @@ interface BaseURLModule {
fun provideVersionServiceFactory( fun provideVersionServiceFactory(
@Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient, @Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient,
json: Json, json: Json,
logger: Logger,
baseURLStorage: BaseURLStorage, baseURLStorage: BaseURLStorage,
): ServiceFactory<VersionService> { ): ServiceFactory<VersionService> {
return RetrofitBuilder(okHttpClient, json).createServiceFactory(baseURLStorage) return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory(
baseURLStorage,
logger
)
} }
} }

View File

@@ -5,6 +5,7 @@ import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.logging.Logger
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import java.io.InputStream import java.io.InputStream
import javax.inject.Named import javax.inject.Named
@@ -13,6 +14,8 @@ import javax.inject.Named
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface GlideModuleEntryPoint { interface GlideModuleEntryPoint {
fun provideLogger(): Logger
@Named(AUTH_OK_HTTP) @Named(AUTH_OK_HTTP)
fun provideOkHttp(): OkHttpClient fun provideOkHttp(): OkHttpClient

View File

@@ -23,6 +23,7 @@ import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSourceImpl import gq.kirmanak.mealient.data.recipes.network.RecipeDataSourceImpl
import gq.kirmanak.mealient.data.recipes.network.RecipeService import gq.kirmanak.mealient.data.recipes.network.RecipeService
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.recipes.images.RecipeModelLoaderFactory import gq.kirmanak.mealient.ui.recipes.images.RecipeModelLoaderFactory
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -61,9 +62,13 @@ interface RecipeModule {
fun provideRecipeServiceFactory( fun provideRecipeServiceFactory(
@Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient, @Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient,
json: Json, json: Json,
logger: Logger,
baseURLStorage: BaseURLStorage, baseURLStorage: BaseURLStorage,
): ServiceFactory<RecipeService> { ): ServiceFactory<RecipeService> {
return RetrofitBuilder(okHttpClient, json).createServiceFactory(baseURLStorage) return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory(
baseURLStorage,
logger
)
} }
@Provides @Provides

View File

@@ -1,22 +1,22 @@
package gq.kirmanak.mealient.extensions package gq.kirmanak.mealient.extensions
import gq.kirmanak.mealient.data.network.NetworkError import gq.kirmanak.mealient.data.network.NetworkError
import gq.kirmanak.mealient.logging.Logger
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import retrofit2.HttpException import retrofit2.HttpException
import retrofit2.Response import retrofit2.Response
import timber.log.Timber
import java.io.InputStream import java.io.InputStream
inline fun <T, reified R> Response<T>.decodeErrorBodyOrNull(json: Json): R? = inline fun <T, reified R> Response<T>.decodeErrorBodyOrNull(json: Json, logger: Logger): R? =
errorBody()?.byteStream()?.let { json.decodeFromStreamOrNull<R>(it) } errorBody()?.byteStream()?.let { json.decodeFromStreamOrNull<R>(it, logger) }
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
inline fun <reified T> Json.decodeFromStreamOrNull(stream: InputStream): T? = inline fun <reified T> Json.decodeFromStreamOrNull(stream: InputStream, logger: Logger): T? =
runCatching { decodeFromStream<T>(stream) } runCatching { decodeFromStream<T>(stream) }
.onFailure { Timber.e(it, "decodeFromStreamOrNull: can't decode") } .onFailure { logger.e(it) { "decodeFromStreamOrNull: can't decode" } }
.getOrNull() .getOrNull()
fun Throwable.mapToNetworkError(): NetworkError = when (this) { fun Throwable.mapToNetworkError(): NetworkError = when (this) {
@@ -24,8 +24,12 @@ fun Throwable.mapToNetworkError(): NetworkError = when (this) {
else -> NetworkError.NoServerConnection(this) else -> NetworkError.NoServerConnection(this)
} }
inline fun <T> logAndMapErrors(block: () -> T, logProvider: () -> String): T = inline fun <T> logAndMapErrors(
logger: Logger,
block: () -> T,
noinline logProvider: () -> String
): T =
runCatchingExceptCancel(block).getOrElse { runCatchingExceptCancel(block).getOrElse {
Timber.e(it, logProvider()) logger.e(it, messageSupplier = logProvider)
throw it.mapToNetworkError() throw it.mapToNetworkError()
} }

View File

@@ -15,6 +15,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.channels.ChannelResult import kotlinx.coroutines.channels.ChannelResult
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.onClosed import kotlinx.coroutines.channels.onClosed
@@ -24,61 +25,60 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
fun SwipeRefreshLayout.refreshRequestFlow(): Flow<Unit> = callbackFlow { fun SwipeRefreshLayout.refreshRequestFlow(logger: Logger): Flow<Unit> = callbackFlow {
Timber.v("refreshRequestFlow() called") logger.v { "refreshRequestFlow() called" }
val listener = SwipeRefreshLayout.OnRefreshListener { val listener = SwipeRefreshLayout.OnRefreshListener {
Timber.v("refreshRequestFlow: listener called") logger.v { "refreshRequestFlow: listener called" }
trySend(Unit).logErrors("refreshesFlow") trySend(Unit).logErrors("refreshesFlow", logger)
} }
setOnRefreshListener(listener) setOnRefreshListener(listener)
awaitClose { awaitClose {
Timber.v("Removing refresh request listener") logger.v { "Removing refresh request listener" }
setOnRefreshListener(null) setOnRefreshListener(null)
} }
} }
fun Activity.setSystemUiVisibility(isVisible: Boolean) { fun Activity.setSystemUiVisibility(isVisible: Boolean, logger: Logger) {
Timber.v("setSystemUiVisibility() called with: isVisible = $isVisible") logger.v { "setSystemUiVisibility() called with: isVisible = $isVisible" }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) setSystemUiVisibilityV30(isVisible) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) setSystemUiVisibilityV30(isVisible, logger)
else setSystemUiVisibilityV1(isVisible) else setSystemUiVisibilityV1(isVisible, logger)
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private fun Activity.setSystemUiVisibilityV1(isVisible: Boolean) { private fun Activity.setSystemUiVisibilityV1(isVisible: Boolean, logger: Logger) {
Timber.v("setSystemUiVisibilityV1() called with: isVisible = $isVisible") logger.v { "setSystemUiVisibilityV1() called with: isVisible = $isVisible" }
window.decorView.systemUiVisibility = if (isVisible) 0 else View.SYSTEM_UI_FLAG_FULLSCREEN window.decorView.systemUiVisibility = if (isVisible) 0 else View.SYSTEM_UI_FLAG_FULLSCREEN
} }
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
private fun Activity.setSystemUiVisibilityV30(isVisible: Boolean) { private fun Activity.setSystemUiVisibilityV30(isVisible: Boolean, logger: Logger) {
Timber.v("setSystemUiVisibilityV30() called with: isVisible = $isVisible") logger.v { "setSystemUiVisibilityV30() called with: isVisible = $isVisible" }
val systemBars = WindowInsets.Type.systemBars() val systemBars = WindowInsets.Type.systemBars()
window.insetsController?.apply { if (isVisible) show(systemBars) else hide(systemBars) } window.insetsController?.apply { if (isVisible) show(systemBars) else hide(systemBars) }
?: Timber.w("setSystemUiVisibilityV30: insets controller is null") ?: logger.w { "setSystemUiVisibilityV30: insets controller is null" }
} }
fun AppCompatActivity.setActionBarVisibility(isVisible: Boolean) { fun AppCompatActivity.setActionBarVisibility(isVisible: Boolean, logger: Logger) {
Timber.v("setActionBarVisibility() called with: isVisible = $isVisible") logger.v { "setActionBarVisibility() called with: isVisible = $isVisible" }
supportActionBar?.apply { if (isVisible) show() else hide() } supportActionBar?.apply { if (isVisible) show() else hide() }
?: Timber.w("setActionBarVisibility: action bar is null") ?: logger.w { "setActionBarVisibility: action bar is null" }
} }
fun TextView.textChangesFlow(): Flow<CharSequence?> = callbackFlow { fun TextView.textChangesFlow(logger: Logger): Flow<CharSequence?> = callbackFlow {
Timber.v("textChangesFlow() called") logger.v { "textChangesFlow() called" }
val textWatcher = doAfterTextChanged { val textWatcher = doAfterTextChanged {
trySend(it).logErrors("textChangesFlow") trySend(it).logErrors("textChangesFlow", logger)
} }
awaitClose { awaitClose {
Timber.d("textChangesFlow: flow is closing") logger.d { "textChangesFlow: flow is closing" }
removeTextChangedListener(textWatcher) removeTextChangedListener(textWatcher)
} }
} }
fun <T> ChannelResult<T>.logErrors(methodName: String): ChannelResult<T> { fun <T> ChannelResult<T>.logErrors(methodName: String, logger: Logger): ChannelResult<T> {
onFailure { Timber.e(it, "$methodName: can't send event") } onFailure { logger.e(it) { "$methodName: can't send event" } }
onClosed { Timber.e(it, "$methodName: flow has been closed") } onClosed { logger.e(it) { "$methodName: flow has been closed" } }
return this return this
} }
@@ -87,29 +87,31 @@ fun EditText.checkIfInputIsEmpty(
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
@StringRes stringId: Int, @StringRes stringId: Int,
trim: Boolean = true, trim: Boolean = true,
logger: Logger,
): String? { ): String? {
val input = if (trim) text?.trim() else text val input = if (trim) text?.trim() else text
val text = input?.toString().orEmpty() val text = input?.toString().orEmpty()
Timber.d("Input text is \"$text\"") logger.d { "Input text is \"$text\"" }
return text.ifEmpty { return text.ifEmpty {
inputLayout.error = resources.getString(stringId) inputLayout.error = resources.getString(stringId)
lifecycleOwner.lifecycleScope.launch { lifecycleOwner.lifecycleScope.launch {
waitUntilNotEmpty() waitUntilNotEmpty(logger)
inputLayout.error = null inputLayout.error = null
} }
null null
} }
} }
suspend fun EditText.waitUntilNotEmpty() { suspend fun EditText.waitUntilNotEmpty(logger: Logger) {
textChangesFlow().filterNotNull().first { it.isNotEmpty() } textChangesFlow(logger).filterNotNull().first { it.isNotEmpty() }
Timber.v("waitUntilNotEmpty() returned") logger.v { "waitUntilNotEmpty() returned" }
} }
fun <T> SharedPreferences.prefsChangeFlow( fun <T> SharedPreferences.prefsChangeFlow(
logger: Logger,
valueReader: SharedPreferences.() -> T, valueReader: SharedPreferences.() -> T,
): Flow<T> = callbackFlow { ): Flow<T> = callbackFlow {
fun sendValue() = trySend(valueReader()).logErrors("prefsChangeFlow") fun sendValue() = trySend(valueReader()).logErrors("prefsChangeFlow", logger)
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> sendValue() } val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> sendValue() }
sendValue() sendValue()
registerOnSharedPreferenceChangeListener(listener) registerOnSharedPreferenceChangeListener(listener)

View File

@@ -10,7 +10,7 @@ import com.bumptech.glide.module.AppGlideModule
import dagger.hilt.android.EntryPointAccessors.fromApplication import dagger.hilt.android.EntryPointAccessors.fromApplication
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.di.GlideModuleEntryPoint import gq.kirmanak.mealient.di.GlideModuleEntryPoint
import timber.log.Timber import gq.kirmanak.mealient.logging.Logger
import java.io.InputStream import java.io.InputStream
@GlideModule @GlideModule
@@ -18,13 +18,13 @@ class MealieGlideModule : AppGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) { override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
super.registerComponents(context, glide, registry) super.registerComponents(context, glide, registry)
Timber.v("registerComponents() called with: context = $context, glide = $glide, registry = $registry") getLogger(context).v { "registerComponents() called with: context = $context, glide = $glide, registry = $registry" }
replaceOkHttp(context, registry) replaceOkHttp(context, registry)
appendRecipeLoader(registry, context) appendRecipeLoader(registry, context)
} }
private fun appendRecipeLoader(registry: Registry, context: Context) { private fun appendRecipeLoader(registry: Registry, context: Context) {
Timber.v("appendRecipeLoader() called with: registry = $registry, context = $context") getLogger(context).v { "appendRecipeLoader() called with: registry = $registry, context = $context" }
registry.append( registry.append(
RecipeSummaryEntity::class.java, RecipeSummaryEntity::class.java,
InputStream::class.java, InputStream::class.java,
@@ -33,17 +33,15 @@ class MealieGlideModule : AppGlideModule() {
} }
private fun replaceOkHttp(context: Context, registry: Registry) { private fun replaceOkHttp(context: Context, registry: Registry) {
Timber.v("replaceOkHttp() called with: context = $context, registry = $registry") getLogger(context).v { "replaceOkHttp() called with: context = $context, registry = $registry" }
val okHttp = getEntryPoint(context).provideOkHttp() val okHttp = getEntryPoint(context).provideOkHttp()
registry.replace( registry.replace(
GlideUrl::class.java, GlideUrl::class.java, InputStream::class.java, OkHttpUrlLoader.Factory(okHttp)
InputStream::class.java,
OkHttpUrlLoader.Factory(okHttp)
) )
} }
private fun getEntryPoint(context: Context): GlideModuleEntryPoint { private fun getEntryPoint(context: Context): GlideModuleEntryPoint =
Timber.v("getEntryPoint() called with: context = $context") fromApplication(context, GlideModuleEntryPoint::class.java)
return fromApplication(context, GlideModuleEntryPoint::class.java)
} private fun getLogger(context: Context): Logger = getEntryPoint(context).provideLogger()
} }

View File

@@ -13,18 +13,23 @@ import com.google.android.material.shape.MaterialShapeDrawable
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.databinding.MainActivityBinding import gq.kirmanak.mealient.databinding.MainActivityBinding
import timber.log.Timber import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: MainActivityBinding private lateinit var binding: MainActivityBinding
private val viewModel by viewModels<MainActivityViewModel>() private val viewModel by viewModels<MainActivityViewModel>()
private val title: String by lazy { getString(R.string.app_name) } private val title: String by lazy { getString(R.string.app_name) }
private val uiState: MainActivityUiState get() = viewModel.uiState private val uiState: MainActivityUiState get() = viewModel.uiState
@Inject
lateinit var logger: Logger
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState") logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" }
binding = MainActivityBinding.inflate(layoutInflater) binding = MainActivityBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.toolbar) setSupportActionBar(binding.toolbar)
@@ -36,7 +41,7 @@ class MainActivity : AppCompatActivity() {
} }
private fun onNavigationItemSelected(menuItem: MenuItem): Boolean { private fun onNavigationItemSelected(menuItem: MenuItem): Boolean {
Timber.v("onNavigationItemSelected() called with: menuItem = $menuItem") logger.v { "onNavigationItemSelected() called with: menuItem = $menuItem" }
menuItem.isChecked = true menuItem.isChecked = true
val deepLink = when (menuItem.itemId) { val deepLink = when (menuItem.itemId) {
R.id.add_recipe -> ADD_RECIPE_DEEP_LINK R.id.add_recipe -> ADD_RECIPE_DEEP_LINK
@@ -49,19 +54,19 @@ class MainActivity : AppCompatActivity() {
} }
private fun onUiStateChange(uiState: MainActivityUiState) { private fun onUiStateChange(uiState: MainActivityUiState) {
Timber.v("onUiStateChange() called with: uiState = $uiState") logger.v { "onUiStateChange() called with: uiState = $uiState" }
supportActionBar?.title = if (uiState.titleVisible) title else null supportActionBar?.title = if (uiState.titleVisible) title else null
binding.navigationView.isVisible = uiState.navigationVisible binding.navigationView.isVisible = uiState.navigationVisible
invalidateOptionsMenu() invalidateOptionsMenu()
} }
private fun setToolbarRoundCorner() { private fun setToolbarRoundCorner() {
Timber.v("setToolbarRoundCorner() called") logger.v { "setToolbarRoundCorner() called" }
val drawables = listOf( val drawables = listOf(
binding.toolbarHolder.background as? MaterialShapeDrawable, binding.toolbarHolder.background as? MaterialShapeDrawable,
binding.toolbar.background as? MaterialShapeDrawable, binding.toolbar.background as? MaterialShapeDrawable,
) )
Timber.d("setToolbarRoundCorner: drawables = $drawables") logger.d { "setToolbarRoundCorner: drawables = $drawables" }
val radius = resources.getDimension(R.dimen.main_activity_toolbar_corner_radius) val radius = resources.getDimension(R.dimen.main_activity_toolbar_corner_radius)
for (drawable in drawables) { for (drawable in drawables) {
drawable?.apply { drawable?.apply {
@@ -72,7 +77,7 @@ class MainActivity : AppCompatActivity() {
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
Timber.v("onCreateOptionsMenu() called with: menu = $menu") logger.v { "onCreateOptionsMenu() called with: menu = $menu" }
menuInflater.inflate(R.menu.main_toolbar, menu) menuInflater.inflate(R.menu.main_toolbar, menu)
menu.findItem(R.id.logout).isVisible = uiState.canShowLogout menu.findItem(R.id.logout).isVisible = uiState.canShowLogout
menu.findItem(R.id.login).isVisible = uiState.canShowLogin menu.findItem(R.id.login).isVisible = uiState.canShowLogin
@@ -80,7 +85,7 @@ class MainActivity : AppCompatActivity() {
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
Timber.v("onOptionsItemSelected() called with: item = $item") logger.v { "onOptionsItemSelected() called with: item = $item" }
val result = when (item.itemId) { val result = when (item.itemId) {
R.id.login -> { R.id.login -> {
navigateDeepLink(AUTH_DEEP_LINK) navigateDeepLink(AUTH_DEEP_LINK)
@@ -96,7 +101,7 @@ class MainActivity : AppCompatActivity() {
} }
private fun navigateDeepLink(deepLink: String) { private fun navigateDeepLink(deepLink: String) {
Timber.v("navigateDeepLink() called with: deepLink = $deepLink") logger.v { "navigateDeepLink() called with: deepLink = $deepLink" }
findNavController(binding.navHost.id).navigate(deepLink.toUri()) findNavController(binding.navHost.id).navigate(deepLink.toUri())
} }

View File

@@ -3,15 +3,16 @@ package gq.kirmanak.mealient.ui.activity
import androidx.lifecycle.* import androidx.lifecycle.*
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MainActivityViewModel @Inject constructor( class MainActivityViewModel @Inject constructor(
private val authRepo: AuthRepo, private val authRepo: AuthRepo,
private val logger: Logger,
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableLiveData(MainActivityUiState()) private val _uiState = MutableLiveData(MainActivityUiState())
@@ -32,7 +33,7 @@ class MainActivityViewModel @Inject constructor(
} }
fun logout() { fun logout() {
Timber.v("logout() called") logger.v { "logout() called" }
viewModelScope.launch { authRepo.logout() } viewModelScope.launch { authRepo.logout() }
} }
} }

View File

@@ -20,8 +20,9 @@ import gq.kirmanak.mealient.databinding.FragmentAddRecipeBinding
import gq.kirmanak.mealient.databinding.ViewSingleInputBinding import gq.kirmanak.mealient.databinding.ViewSingleInputBinding
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
import gq.kirmanak.mealient.extensions.collectWhenViewResumed import gq.kirmanak.mealient.extensions.collectWhenViewResumed
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import timber.log.Timber import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) { class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
@@ -30,9 +31,12 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
private val viewModel by viewModels<AddRecipeViewModel>() private val viewModel by viewModels<AddRecipeViewModel>()
private val activityViewModel by activityViewModels<MainActivityViewModel>() private val activityViewModel by activityViewModels<MainActivityViewModel>()
@Inject
lateinit var logger: Logger
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
activityViewModel.updateUiState { activityViewModel.updateUiState {
it.copy(loginButtonVisible = true, titleVisible = false, navigationVisible = true) it.copy(loginButtonVisible = true, titleVisible = false, navigationVisible = true)
} }
@@ -42,12 +46,12 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
} }
private fun observeAddRecipeResult() { private fun observeAddRecipeResult() {
Timber.v("observeAddRecipeResult() called") logger.v { "observeAddRecipeResult() called" }
collectWhenViewResumed(viewModel.addRecipeResult, ::onRecipeSaveResult) collectWhenViewResumed(viewModel.addRecipeResult, ::onRecipeSaveResult)
} }
private fun onRecipeSaveResult(isSuccessful: Boolean) = with(binding) { private fun onRecipeSaveResult(isSuccessful: Boolean) = with(binding) {
Timber.v("onRecipeSaveResult() called with: isSuccessful = $isSuccessful") logger.v { "onRecipeSaveResult() called with: isSuccessful = $isSuccessful" }
listOf(clearButton, saveRecipeButton).forEach { it.isEnabled = true } listOf(clearButton, saveRecipeButton).forEach { it.isEnabled = true }
@@ -60,12 +64,13 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
} }
private fun setupViews() = with(binding) { private fun setupViews() = with(binding) {
Timber.v("setupViews() called") logger.v { "setupViews() called" }
saveRecipeButton.setOnClickListener { saveRecipeButton.setOnClickListener {
recipeNameInput.checkIfInputIsEmpty( recipeNameInput.checkIfInputIsEmpty(
inputLayout = recipeNameInputLayout, inputLayout = recipeNameInputLayout,
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
stringId = R.string.fragment_add_recipe_name_error stringId = R.string.fragment_add_recipe_name_error,
logger = logger,
) ?: return@setOnClickListener ) ?: return@setOnClickListener
listOf(saveRecipeButton, clearButton).forEach { it.isEnabled = false } listOf(saveRecipeButton, clearButton).forEach { it.isEnabled = false }
@@ -98,7 +103,7 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
} }
private fun inflateInputRow(flow: Flow, @StringRes hintId: Int, text: String? = null) { private fun inflateInputRow(flow: Flow, @StringRes hintId: Int, text: String? = null) {
Timber.v("inflateInputRow() called with: flow = $flow, hintId = $hintId, text = $text") logger.v { "inflateInputRow() called with: flow = $flow, hintId = $hintId, text = $text" }
val fragmentRoot = binding.holder val fragmentRoot = binding.holder
val inputBinding = ViewSingleInputBinding.inflate(layoutInflater, fragmentRoot, false) val inputBinding = ViewSingleInputBinding.inflate(layoutInflater, fragmentRoot, false)
val root = inputBinding.root val root = inputBinding.root
@@ -116,7 +121,7 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
} }
private fun saveValues() = with(binding) { private fun saveValues() = with(binding) {
Timber.v("saveValues() called") logger.v { "saveValues() called" }
val instructions = parseInputRows(instructionsFlow).map { AddRecipeInstruction(text = it) } val instructions = parseInputRows(instructionsFlow).map { AddRecipeInstruction(text = it) }
val ingredients = parseInputRows(ingredientsFlow).map { AddRecipeIngredient(note = it) } val ingredients = parseInputRows(ingredientsFlow).map { AddRecipeIngredient(note = it) }
val settings = AddRecipeSettings( val settings = AddRecipeSettings(
@@ -144,7 +149,7 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
.toList() .toList()
private fun onSavedInputLoaded(request: AddRecipeRequest) = with(binding) { private fun onSavedInputLoaded(request: AddRecipeRequest) = with(binding) {
Timber.v("onSavedInputLoaded() called with: request = $request") logger.v { "onSavedInputLoaded() called with: request = $request" }
recipeNameInput.setText(request.name) recipeNameInput.setText(request.name)
recipeDescriptionInput.setText(request.description) recipeDescriptionInput.setText(request.description)
recipeYieldInput.setText(request.recipeYield) recipeYieldInput.setText(request.recipeYield)
@@ -159,13 +164,13 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
} }
private fun Iterable<String>.showIn(flow: Flow, @StringRes hintId: Int) { private fun Iterable<String>.showIn(flow: Flow, @StringRes hintId: Int) {
Timber.v("showIn() called with: flow = $flow, hintId = $hintId") logger.v { "showIn() called with: flow = $flow, hintId = $hintId" }
flow.removeAllViews() flow.removeAllViews()
forEach { inflateInputRow(flow = flow, hintId = hintId, text = it) } forEach { inflateInputRow(flow = flow, hintId = hintId, text = it) }
} }
private fun Flow.removeAllViews() { private fun Flow.removeAllViews() {
Timber.v("removeAllViews() called") logger.v { "removeAllViews() called" }
for (id in referencedIds.iterator()) { for (id in referencedIds.iterator()) {
val view = binding.holder.findViewById<View>(id) ?: continue val view = binding.holder.findViewById<View>(id) ?: continue
removeView(view) removeView(view)

View File

@@ -6,17 +6,18 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.add.AddRecipeRepo import gq.kirmanak.mealient.data.add.AddRecipeRepo
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class AddRecipeViewModel @Inject constructor( class AddRecipeViewModel @Inject constructor(
private val addRecipeRepo: AddRecipeRepo, private val addRecipeRepo: AddRecipeRepo,
private val logger: Logger,
) : ViewModel() { ) : ViewModel() {
private val _addRecipeResultChannel = Channel<Boolean>(Channel.UNLIMITED) private val _addRecipeResultChannel = Channel<Boolean>(Channel.UNLIMITED)
@@ -27,19 +28,19 @@ class AddRecipeViewModel @Inject constructor(
get() = _preservedAddRecipeRequestChannel.receiveAsFlow() get() = _preservedAddRecipeRequestChannel.receiveAsFlow()
fun loadPreservedRequest() { fun loadPreservedRequest() {
Timber.v("loadPreservedRequest() called") logger.v { "loadPreservedRequest() called" }
viewModelScope.launch { doLoadPreservedRequest() } viewModelScope.launch { doLoadPreservedRequest() }
} }
private suspend fun doLoadPreservedRequest() { private suspend fun doLoadPreservedRequest() {
Timber.v("doLoadPreservedRequest() called") logger.v { "doLoadPreservedRequest() called" }
val request = addRecipeRepo.addRecipeRequestFlow.first() val request = addRecipeRepo.addRecipeRequestFlow.first()
Timber.d("doLoadPreservedRequest: request = $request") logger.d { "doLoadPreservedRequest: request = $request" }
_preservedAddRecipeRequestChannel.send(request) _preservedAddRecipeRequestChannel.send(request)
} }
fun clear() { fun clear() {
Timber.v("clear() called") logger.v { "clear() called" }
viewModelScope.launch { viewModelScope.launch {
addRecipeRepo.clear() addRecipeRepo.clear()
doLoadPreservedRequest() doLoadPreservedRequest()
@@ -47,16 +48,16 @@ class AddRecipeViewModel @Inject constructor(
} }
fun preserve(request: AddRecipeRequest) { fun preserve(request: AddRecipeRequest) {
Timber.v("preserve() called with: request = $request") logger.v { "preserve() called with: request = $request" }
viewModelScope.launch { addRecipeRepo.preserve(request) } viewModelScope.launch { addRecipeRepo.preserve(request) }
} }
fun saveRecipe() { fun saveRecipe() {
Timber.v("saveRecipe() called") logger.v { "saveRecipe() called" }
viewModelScope.launch { viewModelScope.launch {
val isSuccessful = runCatchingExceptCancel { addRecipeRepo.saveRecipe() } val isSuccessful = runCatchingExceptCancel { addRecipeRepo.saveRecipe() }
.fold(onSuccess = { true }, onFailure = { false }) .fold(onSuccess = { true }, onFailure = { false })
Timber.d("saveRecipe: isSuccessful = $isSuccessful") logger.d { "saveRecipe: isSuccessful = $isSuccessful" }
_addRecipeResultChannel.send(isSuccessful) _addRecipeResultChannel.send(isSuccessful)
} }
} }

View File

@@ -12,19 +12,24 @@ import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.data.network.NetworkError import gq.kirmanak.mealient.data.network.NetworkError
import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import timber.log.Timber import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
private val binding by viewBinding(FragmentAuthenticationBinding::bind) private val binding by viewBinding(FragmentAuthenticationBinding::bind)
private val viewModel by viewModels<AuthenticationViewModel>() private val viewModel by viewModels<AuthenticationViewModel>()
private val activityViewModel by activityViewModels<MainActivityViewModel>() private val activityViewModel by activityViewModels<MainActivityViewModel>()
@Inject
lateinit var logger: Logger
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
binding.button.setOnClickListener { onLoginClicked() } binding.button.setOnClickListener { onLoginClicked() }
activityViewModel.updateUiState { activityViewModel.updateUiState {
it.copy(loginButtonVisible = false, titleVisible = true, navigationVisible = false) it.copy(loginButtonVisible = false, titleVisible = true, navigationVisible = false)
@@ -33,12 +38,13 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
} }
private fun onLoginClicked(): Unit = with(binding) { private fun onLoginClicked(): Unit = with(binding) {
Timber.v("onLoginClicked() called") logger.v { "onLoginClicked() called" }
val email: String = emailInput.checkIfInputIsEmpty( val email: String = emailInput.checkIfInputIsEmpty(
inputLayout = emailInputLayout, inputLayout = emailInputLayout,
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
stringId = R.string.fragment_authentication_email_input_empty, stringId = R.string.fragment_authentication_email_input_empty,
logger = logger,
) ?: return ) ?: return
val pass: String = passwordInput.checkIfInputIsEmpty( val pass: String = passwordInput.checkIfInputIsEmpty(
@@ -46,13 +52,14 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
stringId = R.string.fragment_authentication_password_input_empty, stringId = R.string.fragment_authentication_password_input_empty,
trim = false, trim = false,
logger = logger,
) ?: return ) ?: return
viewModel.authenticate(email, pass) viewModel.authenticate(email, pass)
} }
private fun onUiStateChange(uiState: OperationUiState<Unit>) = with(binding) { private fun onUiStateChange(uiState: OperationUiState<Unit>) = with(binding) {
Timber.v("onUiStateChange() called with: authUiState = $uiState") logger.v { "onUiStateChange() called with: authUiState = $uiState" }
if (uiState.isSuccess) { if (uiState.isSuccess) {
findNavController().popBackStack() findNavController().popBackStack()
return return

View File

@@ -7,21 +7,22 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class AuthenticationViewModel @Inject constructor( class AuthenticationViewModel @Inject constructor(
private val authRepo: AuthRepo, private val authRepo: AuthRepo,
private val logger: Logger,
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial()) private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial())
val uiState: LiveData<OperationUiState<Unit>> get() = _uiState val uiState: LiveData<OperationUiState<Unit>> get() = _uiState
fun authenticate(email: String, password: String) { fun authenticate(email: String, password: String) {
Timber.v("authenticate() called with: email = $email, password = $password") logger.v { "authenticate() called with: email = $email, password = $password" }
_uiState.value = OperationUiState.Progress() _uiState.value = OperationUiState.Progress()
viewModelScope.launch { viewModelScope.launch {
val result = runCatchingExceptCancel { authRepo.authenticate(email, password) } val result = runCatchingExceptCancel { authRepo.authenticate(email, password) }

View File

@@ -12,9 +12,10 @@ import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.data.network.NetworkError import gq.kirmanak.mealient.data.network.NetworkError
import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import timber.log.Timber import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class BaseURLFragment : Fragment(R.layout.fragment_base_url) { class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
@@ -23,9 +24,12 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
private val viewModel by viewModels<BaseURLViewModel>() private val viewModel by viewModels<BaseURLViewModel>()
private val activityViewModel by activityViewModels<MainActivityViewModel>() private val activityViewModel by activityViewModels<MainActivityViewModel>()
@Inject
lateinit var logger: Logger
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
binding.button.setOnClickListener(::onProceedClick) binding.button.setOnClickListener(::onProceedClick)
viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange) viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange)
activityViewModel.updateUiState { activityViewModel.updateUiState {
@@ -34,17 +38,18 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
} }
private fun onProceedClick(view: View) { private fun onProceedClick(view: View) {
Timber.v("onProceedClick() called with: view = $view") logger.v { "onProceedClick() called with: view = $view" }
val url = binding.urlInput.checkIfInputIsEmpty( val url = binding.urlInput.checkIfInputIsEmpty(
inputLayout = binding.urlInputLayout, inputLayout = binding.urlInputLayout,
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
stringId = R.string.fragment_baseurl_url_input_empty, stringId = R.string.fragment_baseurl_url_input_empty,
logger = logger,
) ?: return ) ?: return
viewModel.saveBaseUrl(url) viewModel.saveBaseUrl(url)
} }
private fun onUiStateChange(uiState: OperationUiState<Unit>) = with(binding) { private fun onUiStateChange(uiState: OperationUiState<Unit>) = with(binding) {
Timber.v("onUiStateChange() called with: uiState = $uiState") logger.v { "onUiStateChange() called with: uiState = $uiState" }
if (uiState.isSuccess) { if (uiState.isSuccess) {
findNavController().navigate(BaseURLFragmentDirections.actionBaseURLFragmentToRecipesFragment()) findNavController().navigate(BaseURLFragmentDirections.actionBaseURLFragmentToRecipesFragment())
return return

View File

@@ -8,22 +8,23 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
import gq.kirmanak.mealient.data.baseurl.VersionDataSource import gq.kirmanak.mealient.data.baseurl.VersionDataSource
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class BaseURLViewModel @Inject constructor( class BaseURLViewModel @Inject constructor(
private val baseURLStorage: BaseURLStorage, private val baseURLStorage: BaseURLStorage,
private val versionDataSource: VersionDataSource, private val versionDataSource: VersionDataSource,
private val logger: Logger,
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial()) private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial())
val uiState: LiveData<OperationUiState<Unit>> get() = _uiState val uiState: LiveData<OperationUiState<Unit>> get() = _uiState
fun saveBaseUrl(baseURL: String) { fun saveBaseUrl(baseURL: String) {
Timber.v("saveBaseUrl() called with: baseURL = $baseURL") logger.v { "saveBaseUrl() called with: baseURL = $baseURL" }
_uiState.value = OperationUiState.Progress() _uiState.value = OperationUiState.Progress()
val hasPrefix = ALLOWED_PREFIXES.any { baseURL.startsWith(it) } val hasPrefix = ALLOWED_PREFIXES.any { baseURL.startsWith(it) }
val url = baseURL.takeIf { hasPrefix } ?: WITH_PREFIX_FORMAT.format(baseURL) val url = baseURL.takeIf { hasPrefix } ?: WITH_PREFIX_FORMAT.format(baseURL)
@@ -31,13 +32,13 @@ class BaseURLViewModel @Inject constructor(
} }
private suspend fun checkBaseURL(baseURL: String) { private suspend fun checkBaseURL(baseURL: String) {
Timber.v("checkBaseURL() called with: baseURL = $baseURL") logger.v { "checkBaseURL() called with: baseURL = $baseURL" }
val result = runCatchingExceptCancel { val result = runCatchingExceptCancel {
// If it returns proper version info then it must be a Mealie // If it returns proper version info then it must be a Mealie
versionDataSource.getVersionInfo(baseURL) versionDataSource.getVersionInfo(baseURL)
baseURLStorage.storeBaseURL(baseURL) baseURLStorage.storeBaseURL(baseURL)
} }
Timber.i("checkBaseURL: result is $result") logger.i { "checkBaseURL: result is $result" }
_uiState.value = OperationUiState.fromResult(result) _uiState.value = OperationUiState.fromResult(result)
} }

View File

@@ -10,40 +10,45 @@ import by.kirich1409.viewbindingdelegate.viewBinding
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.databinding.FragmentDisclaimerBinding import gq.kirmanak.mealient.databinding.FragmentDisclaimerBinding
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import timber.log.Timber import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) { class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) {
private val binding by viewBinding(FragmentDisclaimerBinding::bind) private val binding by viewBinding(FragmentDisclaimerBinding::bind)
private val viewModel by viewModels<DisclaimerViewModel>() private val viewModel by viewModels<DisclaimerViewModel>()
private val activityViewModel by activityViewModels<MainActivityViewModel>() private val activityViewModel by activityViewModels<MainActivityViewModel>()
@Inject
lateinit var logger: Logger
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState") logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" }
viewModel.isAccepted.observe(this, ::onAcceptStateChange) viewModel.isAccepted.observe(this, ::onAcceptStateChange)
} }
private fun onAcceptStateChange(isAccepted: Boolean) { private fun onAcceptStateChange(isAccepted: Boolean) {
Timber.v("onAcceptStateChange() called with: isAccepted = $isAccepted") logger.v { "onAcceptStateChange() called with: isAccepted = $isAccepted" }
if (isAccepted) navigateNext() if (isAccepted) navigateNext()
} }
private fun navigateNext() { private fun navigateNext() {
Timber.v("navigateNext() called") logger.v { "navigateNext() called" }
findNavController().navigate(DisclaimerFragmentDirections.actionDisclaimerFragmentToBaseURLFragment()) findNavController().navigate(DisclaimerFragmentDirections.actionDisclaimerFragmentToBaseURLFragment())
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
binding.okay.setOnClickListener { binding.okay.setOnClickListener {
Timber.v("onViewCreated: okay clicked") logger.v { "onViewCreated: okay clicked" }
viewModel.acceptDisclaimer() viewModel.acceptDisclaimer()
} }
viewModel.okayCountDown.observe(viewLifecycleOwner) { viewModel.okayCountDown.observe(viewLifecycleOwner) {
Timber.d("onViewCreated: new count $it") logger.d { "onViewCreated: new count $it" }
binding.okay.text = if (it > 0) resources.getQuantityString( binding.okay.text = if (it > 0) resources.getQuantityString(
R.plurals.fragment_disclaimer_button_okay_timer, it, it R.plurals.fragment_disclaimer_button_okay_timer, it, it
) else getString(R.string.fragment_disclaimer_button_okay) ) else getString(R.string.fragment_disclaimer_button_okay)

View File

@@ -4,19 +4,20 @@ import androidx.annotation.VisibleForTesting
import androidx.lifecycle.* import androidx.lifecycle.*
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class DisclaimerViewModel @Inject constructor( class DisclaimerViewModel @Inject constructor(
private val disclaimerStorage: DisclaimerStorage private val disclaimerStorage: DisclaimerStorage,
private val logger: Logger,
) : ViewModel() { ) : ViewModel() {
val isAccepted: LiveData<Boolean> val isAccepted: LiveData<Boolean>
@@ -26,12 +27,12 @@ class DisclaimerViewModel @Inject constructor(
private var isCountDownStarted = false private var isCountDownStarted = false
fun acceptDisclaimer() { fun acceptDisclaimer() {
Timber.v("acceptDisclaimer() called") logger.v { "acceptDisclaimer() called" }
viewModelScope.launch { disclaimerStorage.acceptDisclaimer() } viewModelScope.launch { disclaimerStorage.acceptDisclaimer() }
} }
fun startCountDown() { fun startCountDown() {
Timber.v("startCountDown() called") logger.v { "startCountDown() called" }
if (isCountDownStarted) return if (isCountDownStarted) return
isCountDownStarted = true isCountDownStarted = true
tickerFlow(COUNT_DOWN_TICK_PERIOD_SEC.toLong(), TimeUnit.SECONDS) tickerFlow(COUNT_DOWN_TICK_PERIOD_SEC.toLong(), TimeUnit.SECONDS)
@@ -48,7 +49,7 @@ class DisclaimerViewModel @Inject constructor(
*/ */
@VisibleForTesting @VisibleForTesting
fun tickerFlow(period: Long, timeUnit: TimeUnit) = flow { fun tickerFlow(period: Long, timeUnit: TimeUnit) = flow {
Timber.v("tickerFlow() called with: period = $period, timeUnit = $timeUnit") logger.v { "tickerFlow() called with: period = $period, timeUnit = $timeUnit" }
val periodMillis = timeUnit.toMillis(period) val periodMillis = timeUnit.toMillis(period)
var counter = 0 var counter = 0
while (true) { while (true) {

View File

@@ -4,25 +4,42 @@ import androidx.recyclerview.widget.RecyclerView
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
import timber.log.Timber import javax.inject.Inject
import javax.inject.Singleton
class RecipeViewHolder( class RecipeViewHolder private constructor(
private val logger: Logger,
private val binding: ViewHolderRecipeBinding, private val binding: ViewHolderRecipeBinding,
private val recipeImageLoader: RecipeImageLoader, private val recipeImageLoader: RecipeImageLoader,
private val clickListener: (RecipeSummaryEntity) -> Unit, private val clickListener: (RecipeSummaryEntity) -> Unit,
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
@Singleton
class Factory @Inject constructor(
private val logger: Logger,
) {
fun build(
recipeImageLoader: RecipeImageLoader,
binding: ViewHolderRecipeBinding,
clickListener: (RecipeSummaryEntity) -> Unit,
) = RecipeViewHolder(logger, binding, recipeImageLoader, clickListener)
}
private val loadingPlaceholder by lazy { private val loadingPlaceholder by lazy {
binding.root.resources.getString(R.string.view_holder_recipe_text_placeholder) binding.root.resources.getString(R.string.view_holder_recipe_text_placeholder)
} }
fun bind(item: RecipeSummaryEntity?) { fun bind(item: RecipeSummaryEntity?) {
Timber.v("bind() called with: item = $item") logger.v { "bind() called with: item = $item" }
binding.name.text = item?.name ?: loadingPlaceholder binding.name.text = item?.name ?: loadingPlaceholder
recipeImageLoader.loadRecipeImage(binding.image, item) recipeImageLoader.loadRecipeImage(binding.image, item)
item?.let { entity -> item?.let { entity ->
binding.root.setOnClickListener { binding.root.setOnClickListener {
Timber.d("bind: item clicked $entity") logger.d { "bind: item clicked $entity" }
clickListener(entity) clickListener(entity)
} }
} }

View File

@@ -13,27 +13,34 @@ import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.databinding.FragmentRecipesBinding import gq.kirmanak.mealient.databinding.FragmentRecipesBinding
import gq.kirmanak.mealient.extensions.collectWhenViewResumed import gq.kirmanak.mealient.extensions.collectWhenViewResumed
import gq.kirmanak.mealient.extensions.refreshRequestFlow import gq.kirmanak.mealient.extensions.refreshRequestFlow
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
import gq.kirmanak.mealient.ui.recipes.images.RecipePreloaderFactory import gq.kirmanak.mealient.ui.recipes.images.RecipePreloaderFactory
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class RecipesFragment : Fragment(R.layout.fragment_recipes) { class RecipesFragment : Fragment(R.layout.fragment_recipes) {
private val binding by viewBinding(FragmentRecipesBinding::bind) private val binding by viewBinding(FragmentRecipesBinding::bind)
private val viewModel by viewModels<RecipeViewModel>() private val viewModel by viewModels<RecipeViewModel>()
private val activityViewModel by activityViewModels<MainActivityViewModel>() private val activityViewModel by activityViewModels<MainActivityViewModel>()
@Inject
lateinit var logger: Logger
@Inject @Inject
lateinit var recipeImageLoader: RecipeImageLoader lateinit var recipeImageLoader: RecipeImageLoader
@Inject
lateinit var recipePagingAdapterFactory: RecipesPagingAdapter.Factory
@Inject @Inject
lateinit var recipePreloaderFactory: RecipePreloaderFactory lateinit var recipePreloaderFactory: RecipePreloaderFactory
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
activityViewModel.updateUiState { activityViewModel.updateUiState {
it.copy(loginButtonVisible = true, titleVisible = false, navigationVisible = true) it.copy(loginButtonVisible = true, titleVisible = false, navigationVisible = true)
} }
@@ -41,7 +48,7 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
} }
private fun navigateToRecipeInfo(recipeSummaryEntity: RecipeSummaryEntity) { private fun navigateToRecipeInfo(recipeSummaryEntity: RecipeSummaryEntity) {
Timber.v("navigateToRecipeInfo() called with: recipeSummaryEntity = $recipeSummaryEntity") logger.v { "navigateToRecipeInfo() called with: recipeSummaryEntity = $recipeSummaryEntity" }
findNavController().navigate( findNavController().navigate(
RecipesFragmentDirections.actionRecipesFragmentToRecipeInfoFragment( RecipesFragmentDirections.actionRecipesFragmentToRecipeInfoFragment(
recipeSlug = recipeSummaryEntity.slug, recipeSlug = recipeSummaryEntity.slug,
@@ -51,29 +58,32 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
} }
private fun setupRecipeAdapter() { private fun setupRecipeAdapter() {
Timber.v("setupRecipeAdapter() called") logger.v { "setupRecipeAdapter() called" }
val recipesAdapter = RecipesPagingAdapter(recipeImageLoader, ::navigateToRecipeInfo) val recipesAdapter = recipePagingAdapterFactory.build(
recipeImageLoader = recipeImageLoader,
clickListener = ::navigateToRecipeInfo
)
with(binding.recipes) { with(binding.recipes) {
adapter = recipesAdapter adapter = recipesAdapter
addOnScrollListener(recipePreloaderFactory.create(recipesAdapter)) addOnScrollListener(recipePreloaderFactory.create(recipesAdapter))
} }
collectWhenViewResumed(viewModel.pagingData) { collectWhenViewResumed(viewModel.pagingData) {
Timber.v("setupRecipeAdapter: received data update") logger.v { "setupRecipeAdapter: received data update" }
recipesAdapter.submitData(lifecycle, it) recipesAdapter.submitData(lifecycle, it)
} }
collectWhenViewResumed(recipesAdapter.onPagesUpdatedFlow) { collectWhenViewResumed(recipesAdapter.onPagesUpdatedFlow) {
Timber.v("setupRecipeAdapter: pages updated") logger.v { "setupRecipeAdapter: pages updated" }
binding.refresher.isRefreshing = false binding.refresher.isRefreshing = false
} }
collectWhenViewResumed(binding.refresher.refreshRequestFlow()) { collectWhenViewResumed(binding.refresher.refreshRequestFlow(logger)) {
Timber.v("setupRecipeAdapter: received refresh request") logger.v { "setupRecipeAdapter: received refresh request" }
recipesAdapter.refresh() recipesAdapter.refresh()
} }
} }
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
Timber.v("onDestroyView() called") logger.v { "onDestroyView() called" }
// Prevent RV leaking through mObservers list in adapter // Prevent RV leaking through mObservers list in adapter
binding.recipes.adapter = null binding.recipes.adapter = null
} }

View File

@@ -6,24 +6,40 @@ import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
import timber.log.Timber import javax.inject.Inject
import javax.inject.Singleton
class RecipesPagingAdapter( class RecipesPagingAdapter private constructor(
private val logger: Logger,
private val recipeImageLoader: RecipeImageLoader, private val recipeImageLoader: RecipeImageLoader,
private val recipeViewHolderFactory: RecipeViewHolder.Factory,
private val clickListener: (RecipeSummaryEntity) -> Unit private val clickListener: (RecipeSummaryEntity) -> Unit
) : PagingDataAdapter<RecipeSummaryEntity, RecipeViewHolder>(RecipeDiffCallback) { ) : PagingDataAdapter<RecipeSummaryEntity, RecipeViewHolder>(RecipeDiffCallback) {
@Singleton
class Factory @Inject constructor(
private val logger: Logger,
private val recipeViewHolderFactory: RecipeViewHolder.Factory,
) {
fun build(
recipeImageLoader: RecipeImageLoader,
clickListener: (RecipeSummaryEntity) -> Unit,
) = RecipesPagingAdapter(logger, recipeImageLoader, recipeViewHolderFactory, clickListener)
}
override fun onBindViewHolder(holder: RecipeViewHolder, position: Int) { override fun onBindViewHolder(holder: RecipeViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
holder.bind(item) holder.bind(item)
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeViewHolder {
Timber.v("onCreateViewHolder() called with: parent = $parent, viewType = $viewType") logger.v { "onCreateViewHolder() called with: parent = $parent, viewType = $viewType" }
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
val binding = ViewHolderRecipeBinding.inflate(inflater, parent, false) val binding = ViewHolderRecipeBinding.inflate(inflater, parent, false)
return RecipeViewHolder(binding, recipeImageLoader, clickListener) return recipeViewHolderFactory.build(recipeImageLoader, binding, clickListener)
} }
private object RecipeDiffCallback : DiffUtil.ItemCallback<RecipeSummaryEntity>() { private object RecipeDiffCallback : DiffUtil.ItemCallback<RecipeSummaryEntity>() {

View File

@@ -6,17 +6,18 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import dagger.hilt.android.scopes.FragmentScoped import dagger.hilt.android.scopes.FragmentScoped
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import timber.log.Timber import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject import javax.inject.Inject
@FragmentScoped @FragmentScoped
class RecipeImageLoaderImpl @Inject constructor( class RecipeImageLoaderImpl @Inject constructor(
private val fragment: Fragment, private val fragment: Fragment,
private val requestOptions: RequestOptions, private val requestOptions: RequestOptions,
private val logger: Logger,
) : RecipeImageLoader { ) : RecipeImageLoader {
override fun loadRecipeImage(view: ImageView, recipe: RecipeSummaryEntity?) { override fun loadRecipeImage(view: ImageView, recipe: RecipeSummaryEntity?) {
Timber.v("loadRecipeImage() called with: view = $view, recipe = $recipe") logger.v { "loadRecipeImage() called with: view = $view, recipe = $recipe" }
Glide.with(fragment).load(recipe).apply(requestOptions).into(view) Glide.with(fragment).load(recipe).apply(requestOptions).into(view)
} }
} }

View File

@@ -7,16 +7,32 @@ import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.stream.BaseGlideUrlLoader import com.bumptech.glide.load.model.stream.BaseGlideUrlLoader
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import timber.log.Timber
import java.io.InputStream import java.io.InputStream
import javax.inject.Inject
import javax.inject.Singleton
class RecipeModelLoader( class RecipeModelLoader private constructor(
private val recipeImageUrlProvider: RecipeImageUrlProvider, private val recipeImageUrlProvider: RecipeImageUrlProvider,
private val logger: Logger,
concreteLoader: ModelLoader<GlideUrl, InputStream>, concreteLoader: ModelLoader<GlideUrl, InputStream>,
cache: ModelCache<RecipeSummaryEntity, GlideUrl>, cache: ModelCache<RecipeSummaryEntity, GlideUrl>,
) : BaseGlideUrlLoader<RecipeSummaryEntity>(concreteLoader, cache) { ) : BaseGlideUrlLoader<RecipeSummaryEntity>(concreteLoader, cache) {
@Singleton
class Factory @Inject constructor(
private val recipeImageUrlProvider: RecipeImageUrlProvider,
private val logger: Logger,
) {
fun build(
concreteLoader: ModelLoader<GlideUrl, InputStream>,
cache: ModelCache<RecipeSummaryEntity, GlideUrl>,
) = RecipeModelLoader(recipeImageUrlProvider, logger, concreteLoader, cache)
}
override fun handles(model: RecipeSummaryEntity): Boolean = true override fun handles(model: RecipeSummaryEntity): Boolean = true
override fun getUrl( override fun getUrl(
@@ -25,7 +41,7 @@ class RecipeModelLoader(
height: Int, height: Int,
options: Options? options: Options?
): String? { ): String? {
Timber.v("getUrl() called with: model = $model, width = $width, height = $height, options = $options") logger.v { "getUrl() called with: model = $model, width = $width, height = $height, options = $options" }
return runBlocking { recipeImageUrlProvider.generateImageUrl(model?.slug) } return runBlocking { recipeImageUrlProvider.generateImageUrl(model?.slug) }
} }
} }

View File

@@ -1,24 +1,24 @@
package gq.kirmanak.mealient.ui.recipes.images package gq.kirmanak.mealient.ui.recipes.images
import com.bumptech.glide.load.model.* import com.bumptech.glide.load.model.*
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import timber.log.Timber import gq.kirmanak.mealient.logging.Logger
import java.io.InputStream import java.io.InputStream
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class RecipeModelLoaderFactory @Inject constructor( class RecipeModelLoaderFactory @Inject constructor(
private val recipeImageUrlProvider: RecipeImageUrlProvider, private val recipeModelLoaderFactory: RecipeModelLoader.Factory,
private val logger: Logger,
) : ModelLoaderFactory<RecipeSummaryEntity, InputStream> { ) : ModelLoaderFactory<RecipeSummaryEntity, InputStream> {
private val cache = ModelCache<RecipeSummaryEntity, GlideUrl>() private val cache = ModelCache<RecipeSummaryEntity, GlideUrl>()
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<RecipeSummaryEntity, InputStream> { override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<RecipeSummaryEntity, InputStream> {
Timber.v("build() called with: multiFactory = $multiFactory") logger.v { "build() called with: multiFactory = $multiFactory" }
val concreteLoader = multiFactory.build(GlideUrl::class.java, InputStream::class.java) val concreteLoader = multiFactory.build(GlideUrl::class.java, InputStream::class.java)
return RecipeModelLoader(recipeImageUrlProvider, concreteLoader, cache) return recipeModelLoaderFactory.build(concreteLoader, cache)
} }
override fun teardown() { override fun teardown() {

View File

@@ -8,22 +8,23 @@ import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import dagger.hilt.android.scopes.FragmentScoped import dagger.hilt.android.scopes.FragmentScoped
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import timber.log.Timber import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject import javax.inject.Inject
class RecipePreloadModelProvider( class RecipePreloadModelProvider(
private val adapter: PagingDataAdapter<RecipeSummaryEntity, *>, private val adapter: PagingDataAdapter<RecipeSummaryEntity, *>,
private val fragment: Fragment, private val fragment: Fragment,
private val requestOptions: RequestOptions, private val requestOptions: RequestOptions,
private val logger: Logger,
) : ListPreloader.PreloadModelProvider<RecipeSummaryEntity> { ) : ListPreloader.PreloadModelProvider<RecipeSummaryEntity> {
override fun getPreloadItems(position: Int): List<RecipeSummaryEntity> { override fun getPreloadItems(position: Int): List<RecipeSummaryEntity> {
Timber.v("getPreloadItems() called with: position = $position") logger.v { "getPreloadItems() called with: position = $position" }
return adapter.peek(position)?.let { listOf(it) } ?: emptyList() return adapter.peek(position)?.let { listOf(it) } ?: emptyList()
} }
override fun getPreloadRequestBuilder(item: RecipeSummaryEntity): RequestBuilder<*> { override fun getPreloadRequestBuilder(item: RecipeSummaryEntity): RequestBuilder<*> {
Timber.v("getPreloadRequestBuilder() called with: item = $item") logger.v { "getPreloadRequestBuilder() called with: item = $item" }
return Glide.with(fragment).load(item).apply(requestOptions) return Glide.with(fragment).load(item).apply(requestOptions)
} }
@@ -31,10 +32,11 @@ class RecipePreloadModelProvider(
class Factory @Inject constructor( class Factory @Inject constructor(
private val fragment: Fragment, private val fragment: Fragment,
private val requestOptions: RequestOptions, private val requestOptions: RequestOptions,
private val logger: Logger,
) { ) {
fun create( fun create(
adapter: PagingDataAdapter<RecipeSummaryEntity, *>, adapter: PagingDataAdapter<RecipeSummaryEntity, *>,
) = RecipePreloadModelProvider(adapter, fragment, requestOptions) ) = RecipePreloadModelProvider(adapter, fragment, requestOptions, logger)
} }
} }

View File

@@ -14,8 +14,8 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.databinding.FragmentRecipeInfoBinding import gq.kirmanak.mealient.databinding.FragmentRecipeInfoBinding
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -24,8 +24,17 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
private val binding by viewBinding(FragmentRecipeInfoBinding::bind) private val binding by viewBinding(FragmentRecipeInfoBinding::bind)
private val arguments by navArgs<RecipeInfoFragmentArgs>() private val arguments by navArgs<RecipeInfoFragmentArgs>()
private val viewModel by viewModels<RecipeInfoViewModel>() private val viewModel by viewModels<RecipeInfoViewModel>()
private val ingredientsAdapter = RecipeIngredientsAdapter() private val ingredientsAdapter by lazy { recipeIngredientsAdapterFactory.build() }
private val instructionsAdapter = RecipeInstructionsAdapter() private val instructionsAdapter by lazy { recipeInstructionsAdapterFactory.build() }
@Inject
lateinit var recipeInstructionsAdapterFactory: RecipeInstructionsAdapter.Factory
@Inject
lateinit var recipeIngredientsAdapterFactory: RecipeIngredientsAdapter.Factory
@Inject
lateinit var logger: Logger
@Inject @Inject
lateinit var recipeImageLoader: RecipeImageLoader lateinit var recipeImageLoader: RecipeImageLoader
@@ -35,13 +44,13 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
Timber.v("onCreateView() called") logger.v { "onCreateView() called" }
return FragmentRecipeInfoBinding.inflate(inflater, container, false).root return FragmentRecipeInfoBinding.inflate(inflater, container, false).root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
Timber.v("onViewCreated() called") logger.v { "onViewCreated() called" }
with(binding) { with(binding) {
ingredientsList.adapter = ingredientsAdapter ingredientsList.adapter = ingredientsAdapter
@@ -55,7 +64,7 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
} }
private fun onUiStateChange(uiState: RecipeInfoUiState) = with(binding) { private fun onUiStateChange(uiState: RecipeInfoUiState) = with(binding) {
Timber.v("onUiStateChange() called") logger.v { "onUiStateChange() called" }
ingredientsHolder.isVisible = uiState.areIngredientsVisible ingredientsHolder.isVisible = uiState.areIngredientsVisible
instructionsGroup.isVisible = uiState.areInstructionsVisible instructionsGroup.isVisible = uiState.areInstructionsVisible
uiState.recipeInfo?.let { uiState.recipeInfo?.let {
@@ -72,7 +81,7 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
Timber.v("onDestroyView() called") logger.v { "onDestroyView() called" }
// Prevent RV leaking through mObservers list in adapter // Prevent RV leaking through mObservers list in adapter
with(binding) { with(binding) {
ingredientsList.adapter = null ingredientsList.adapter = null

View File

@@ -7,32 +7,33 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class RecipeInfoViewModel @Inject constructor( class RecipeInfoViewModel @Inject constructor(
private val recipeRepo: RecipeRepo, private val recipeRepo: RecipeRepo,
private val logger: Logger,
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableLiveData(RecipeInfoUiState()) private val _uiState = MutableLiveData(RecipeInfoUiState())
val uiState: LiveData<RecipeInfoUiState> get() = _uiState val uiState: LiveData<RecipeInfoUiState> get() = _uiState
fun loadRecipeInfo(recipeId: Long, recipeSlug: String) { fun loadRecipeInfo(recipeId: Long, recipeSlug: String) {
Timber.v("loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug") logger.v { "loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug" }
_uiState.value = RecipeInfoUiState() _uiState.value = RecipeInfoUiState()
viewModelScope.launch { viewModelScope.launch {
runCatchingExceptCancel { recipeRepo.loadRecipeInfo(recipeId, recipeSlug) } runCatchingExceptCancel { recipeRepo.loadRecipeInfo(recipeId, recipeSlug) }
.onSuccess { .onSuccess {
Timber.d("loadRecipeInfo: received recipe info = $it") logger.d { "loadRecipeInfo: received recipe info = $it" }
_uiState.value = RecipeInfoUiState( _uiState.value = RecipeInfoUiState(
areIngredientsVisible = it.recipeIngredients.isNotEmpty(), areIngredientsVisible = it.recipeIngredients.isNotEmpty(),
areInstructionsVisible = it.recipeInstructions.isNotEmpty(), areInstructionsVisible = it.recipeInstructions.isNotEmpty(),
recipeInfo = it, recipeInfo = it,
) )
} }
.onFailure { Timber.e(it, "loadRecipeInfo: can't load recipe info") } .onFailure { logger.e(it) { "loadRecipeInfo: can't load recipe info" } }
} }
} }
} }

View File

@@ -7,17 +7,40 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
import gq.kirmanak.mealient.databinding.ViewHolderIngredientBinding import gq.kirmanak.mealient.databinding.ViewHolderIngredientBinding
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.recipes.info.RecipeIngredientsAdapter.RecipeIngredientViewHolder import gq.kirmanak.mealient.ui.recipes.info.RecipeIngredientsAdapter.RecipeIngredientViewHolder
import timber.log.Timber import javax.inject.Inject
import javax.inject.Singleton
class RecipeIngredientsAdapter : class RecipeIngredientsAdapter private constructor(
ListAdapter<RecipeIngredientEntity, RecipeIngredientViewHolder>(RecipeIngredientDiffCallback) { private val recipeIngredientViewHolderFactory: RecipeIngredientViewHolder.Factory,
private val logger: Logger,
) : ListAdapter<RecipeIngredientEntity, RecipeIngredientViewHolder>(RecipeIngredientDiffCallback) {
class RecipeIngredientViewHolder( @Singleton
private val binding: ViewHolderIngredientBinding class Factory @Inject constructor(
private val recipeIngredientViewHolderFactory: RecipeIngredientViewHolder.Factory,
private val logger: Logger,
) {
fun build() = RecipeIngredientsAdapter(recipeIngredientViewHolderFactory, logger)
}
class RecipeIngredientViewHolder private constructor(
private val binding: ViewHolderIngredientBinding,
private val logger: Logger,
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
@Singleton
class Factory @Inject constructor(
private val logger: Logger,
) {
fun build(binding: ViewHolderIngredientBinding) =
RecipeIngredientViewHolder(binding, logger)
}
fun bind(item: RecipeIngredientEntity) { fun bind(item: RecipeIngredientEntity) {
Timber.v("bind() called with: item = $item") logger.v { "bind() called with: item = $item" }
binding.checkBox.text = item.note binding.checkBox.text = item.note
} }
} }
@@ -35,17 +58,17 @@ class RecipeIngredientsAdapter :
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeIngredientViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeIngredientViewHolder {
Timber.v("onCreateViewHolder() called with: parent = $parent, viewType = $viewType") logger.v { "onCreateViewHolder() called with: parent = $parent, viewType = $viewType" }
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return RecipeIngredientViewHolder( return recipeIngredientViewHolderFactory.build(
ViewHolderIngredientBinding.inflate(inflater, parent, false) ViewHolderIngredientBinding.inflate(inflater, parent, false)
) )
} }
override fun onBindViewHolder(holder: RecipeIngredientViewHolder, position: Int) { override fun onBindViewHolder(holder: RecipeIngredientViewHolder, position: Int) {
Timber.v("onBindViewHolder() called with: holder = $holder, position = $position") logger.v { "onBindViewHolder() called with: holder = $holder, position = $position" }
val item = getItem(position) val item = getItem(position)
Timber.d("onBindViewHolder: item is $item") logger.d { "onBindViewHolder: item is $item" }
holder.bind(item) holder.bind(item)
} }
} }

View File

@@ -8,11 +8,23 @@ import androidx.recyclerview.widget.RecyclerView
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
import gq.kirmanak.mealient.databinding.ViewHolderInstructionBinding import gq.kirmanak.mealient.databinding.ViewHolderInstructionBinding
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.recipes.info.RecipeInstructionsAdapter.RecipeInstructionViewHolder import gq.kirmanak.mealient.ui.recipes.info.RecipeInstructionsAdapter.RecipeInstructionViewHolder
import timber.log.Timber import javax.inject.Inject
import javax.inject.Singleton
class RecipeInstructionsAdapter : class RecipeInstructionsAdapter private constructor(
ListAdapter<RecipeInstructionEntity, RecipeInstructionViewHolder>(RecipeInstructionDiffCallback) { private val logger: Logger,
private val recipeInstructionViewHolderFactory: RecipeInstructionViewHolder.Factory,
) : ListAdapter<RecipeInstructionEntity, RecipeInstructionViewHolder>(RecipeInstructionDiffCallback) {
@Singleton
class Factory @Inject constructor(
private val logger: Logger,
private val recipeInstructionViewHolderFactory: RecipeInstructionViewHolder.Factory,
) {
fun build() = RecipeInstructionsAdapter(logger, recipeInstructionViewHolderFactory)
}
private object RecipeInstructionDiffCallback : private object RecipeInstructionDiffCallback :
DiffUtil.ItemCallback<RecipeInstructionEntity>() { DiffUtil.ItemCallback<RecipeInstructionEntity>() {
@@ -27,11 +39,19 @@ class RecipeInstructionsAdapter :
): Boolean = oldItem == newItem ): Boolean = oldItem == newItem
} }
class RecipeInstructionViewHolder( class RecipeInstructionViewHolder private constructor(
private val binding: ViewHolderInstructionBinding private val binding: ViewHolderInstructionBinding,
private val logger: Logger,
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
@Singleton
class Factory @Inject constructor(private val logger: Logger) {
fun build(binding: ViewHolderInstructionBinding) =
RecipeInstructionViewHolder(binding, logger)
}
fun bind(item: RecipeInstructionEntity, position: Int) { fun bind(item: RecipeInstructionEntity, position: Int) {
Timber.v("bind() called with: item = $item, position = $position") logger.v { "bind() called with: item = $item, position = $position" }
binding.step.text = binding.root.resources.getString( binding.step.text = binding.root.resources.getString(
R.string.view_holder_recipe_instructions_step, position + 1 R.string.view_holder_recipe_instructions_step, position + 1
) )
@@ -40,17 +60,17 @@ class RecipeInstructionsAdapter :
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeInstructionViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeInstructionViewHolder {
Timber.v("onCreateViewHolder() called with: parent = $parent, viewType = $viewType") logger.v { "onCreateViewHolder() called with: parent = $parent, viewType = $viewType" }
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return RecipeInstructionViewHolder( return recipeInstructionViewHolderFactory.build(
ViewHolderInstructionBinding.inflate(inflater, parent, false) ViewHolderInstructionBinding.inflate(inflater, parent, false),
) )
} }
override fun onBindViewHolder(holder: RecipeInstructionViewHolder, position: Int) { override fun onBindViewHolder(holder: RecipeInstructionViewHolder, position: Int) {
Timber.v("onBindViewHolder() called with: holder = $holder, position = $position") logger.v { "onBindViewHolder() called with: holder = $holder, position = $position" }
val item = getItem(position) val item = getItem(position)
Timber.d("onBindViewHolder: item is $item") logger.d { "onBindViewHolder: item is $item" }
holder.bind(item, position) holder.bind(item, position)
} }
} }

View File

@@ -11,38 +11,43 @@ import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.extensions.setActionBarVisibility import gq.kirmanak.mealient.extensions.setActionBarVisibility
import gq.kirmanak.mealient.extensions.setSystemUiVisibility import gq.kirmanak.mealient.extensions.setSystemUiVisibility
import timber.log.Timber import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class SplashFragment : Fragment(R.layout.fragment_splash) { class SplashFragment : Fragment(R.layout.fragment_splash) {
private val viewModel by viewModels<SplashViewModel>() private val viewModel by viewModels<SplashViewModel>()
@Inject
lateinit var logger: Logger
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState") logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" }
viewModel.nextDestination.observe(this, ::onNextDestination) viewModel.nextDestination.observe(this, ::onNextDestination)
} }
private fun onNextDestination(navDirections: NavDirections) { private fun onNextDestination(navDirections: NavDirections) {
Timber.v("onNextDestination() called with: navDirections = $navDirections") logger.v { "onNextDestination() called with: navDirections = $navDirections" }
findNavController().navigate(navDirections) findNavController().navigate(navDirections)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
changeFullscreenState(true) changeFullscreenState(true)
} }
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
Timber.v("onDestroyView() called") logger.v { "onDestroyView() called" }
changeFullscreenState(false) changeFullscreenState(false)
} }
private fun changeFullscreenState(isFullscreen: Boolean) { private fun changeFullscreenState(isFullscreen: Boolean) {
Timber.v("changeFullscreenState() called with: isFullscreen = $isFullscreen") logger.v { "changeFullscreenState() called with: isFullscreen = $isFullscreen" }
(activity as? AppCompatActivity)?.setActionBarVisibility(!isFullscreen) (activity as? AppCompatActivity)?.setActionBarVisibility(!isFullscreen, logger)
activity?.setSystemUiVisibility(!isFullscreen) activity?.setSystemUiVisibility(!isFullscreen, logger)
} }
} }

View File

@@ -41,8 +41,6 @@ retrofitKotlinxSerialization = "0.8.0"
kotlinxSerialization = "1.3.3" kotlinxSerialization = "1.3.3"
# https://github.com/square/okhttp/tags # https://github.com/square/okhttp/tags
okhttp = "4.10.0" okhttp = "4.10.0"
# https://github.com/JakeWharton/timber/releases
timber = "5.0.1"
# https://developer.android.com/jetpack/androidx/releases/paging # https://developer.android.com/jetpack/androidx/releases/paging
paging = "3.1.1" paging = "3.1.1"
# https://developer.android.com/jetpack/androidx/releases/room # https://developer.android.com/jetpack/androidx/releases/room
@@ -140,7 +138,6 @@ androidx-test-junit = { group = "androidx.test.ext", name = "junit-ktx", version
androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "security" } androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "security" }
jakewharton-retrofitSerialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinxSerialization" } jakewharton-retrofitSerialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinxSerialization" }
jakewharton-timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
squareup-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } squareup-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }