diff --git a/LICENSE b/LICENSE index 3d8a2c7a..1af01810 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Kotlin Android Open Source +Copyright (c) 2019-2021 Kotlin Android Open Source Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1c27ad9f..f73e6abe 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -67,6 +67,7 @@ dependencies { implementation(deps.koin.android) debugImplementation(deps.squareup.leakCanary) + implementation(deps.timber) testImplementation(deps.test.junit) androidTestImplementation(deps.test.androidxJunit) diff --git a/app/src/main/java/com/hoc/flowmvi/App.kt b/app/src/main/java/com/hoc/flowmvi/App.kt index 2061ce47..f703fc24 100644 --- a/app/src/main/java/com/hoc/flowmvi/App.kt +++ b/app/src/main/java/com/hoc/flowmvi/App.kt @@ -13,6 +13,7 @@ import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.context.startKoin import org.koin.core.logger.Level +import timber.log.Timber import kotlin.time.ExperimentalTime @FlowPreview @@ -37,6 +38,12 @@ class App : Application() { override fun onCreate() { super.onCreate() + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } else { + // TODO(Timber): plant release tree + } + startKoin { androidContext(this@App) diff --git a/buildSrc/src/main/kotlin/deps.kt b/buildSrc/src/main/kotlin/deps.kt index a13252df..1cbfc9cf 100644 --- a/buildSrc/src/main/kotlin/deps.kt +++ b/buildSrc/src/main/kotlin/deps.kt @@ -69,6 +69,7 @@ object deps { const val coil = "io.coil-kt:coil:1.2.1" const val viewBindingDelegate = "com.github.hoc081098:ViewBindingDelegate:1.2.0" const val flowExt = "io.github.hoc081098:FlowExt:0.0.7-SNAPSHOT" + const val timber = "com.jakewharton.timber:timber:5.0.1" object arrow { private const val version = "1.0.1" diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 25ef57b0..351e1704 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -49,5 +49,7 @@ dependencies { implementation(deps.lifecycle.commonJava8) implementation(deps.lifecycle.runtimeKtx) + implementation(deps.timber) + addUnitTest() } diff --git a/core/src/main/java/com/hoc/flowmvi/core/CollectIn.kt b/core/src/main/java/com/hoc/flowmvi/core/CollectIn.kt index 13642a10..c80ff5fe 100644 --- a/core/src/main/java/com/hoc/flowmvi/core/CollectIn.kt +++ b/core/src/main/java/com/hoc/flowmvi/core/CollectIn.kt @@ -1,6 +1,5 @@ package com.hoc.flowmvi.core -import android.util.Log import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner @@ -10,6 +9,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +import timber.log.Timber inline fun Flow.collectIn( owner: LifecycleOwner, @@ -17,7 +17,7 @@ inline fun Flow.collectIn( crossinline action: suspend (value: T) -> Unit, ): Job = owner.lifecycleScope.launch { owner.repeatOnLifecycle(state = minActiveState) { - Log.d("collectIn", "Start collecting $owner $minActiveState...") + Timber.d("Start collecting $owner $minActiveState...") collect { action(it) } } } diff --git a/core/src/main/java/com/hoc/flowmvi/core/FlowBinding.kt b/core/src/main/java/com/hoc/flowmvi/core/FlowBinding.kt index 4d2cccf5..29528fb0 100644 --- a/core/src/main/java/com/hoc/flowmvi/core/FlowBinding.kt +++ b/core/src/main/java/com/hoc/flowmvi/core/FlowBinding.kt @@ -2,7 +2,6 @@ package com.hoc.flowmvi.core import android.content.Context import android.os.Looper -import android.util.Log import android.view.View import android.widget.EditText import android.widget.Toast @@ -17,6 +16,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.take +import timber.log.Timber import kotlin.coroutines.EmptyCoroutineContext internal fun checkMainThread() { @@ -35,7 +35,7 @@ fun EditText.firstChange(): Flow { awaitClose { Dispatchers.Main.dispatch(EmptyCoroutineContext) { removeTextChangedListener(listener) - Log.d("###", "removeTextChangedListener $listener ${this@firstChange}") + Timber.d("removeTextChangedListener $listener $this") } } }.take(1) diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 728d958d..412a1f59 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -52,5 +52,7 @@ dependencies { implementation(deps.koin.core) implementation(deps.arrow.core) + implementation(deps.timber) + addUnitTest() } diff --git a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt index d86a82c8..f2da9786 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt @@ -1,6 +1,5 @@ package com.hoc.flowmvi.data -import android.util.Log import arrow.core.Either import arrow.core.left import arrow.core.leftWiden @@ -24,6 +23,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.scan import kotlinx.coroutines.withContext +import timber.log.Timber import java.io.IOException import kotlin.time.Duration import kotlin.time.ExperimentalTime @@ -54,7 +54,7 @@ internal class UserRepositoryImpl( factor = 2.0, shouldRetry = { it is IOException } ) { - Log.d("###", "[USER_REPO] Retry times=$it") + Timber.d("[USER_REPO] Retry times=$it") userApiService.getUsers().map(responseToDomain) } } @@ -64,7 +64,7 @@ internal class UserRepositoryImpl( val initial = getUsersFromRemote() changesFlow - .onEach { Log.d("###", "[USER_REPO] Change=$it") } + .onEach { Timber.d("[USER_REPO] Change=$it") } .scan(initial) { acc, change -> when (change) { is Change.Removed -> acc.filter { it.id != change.removed.id } @@ -72,38 +72,45 @@ internal class UserRepositoryImpl( is Change.Added -> acc + change.user } } - .onEach { Log.d("###", "[USER_REPO] Emit users.size=${it.size} ") } + .onEach { Timber.d("[USER_REPO] Emit users.size=${it.size} ") } .let { emitAll(it) } } .map { it.right().leftWiden>() } - .catch { emit(errorMapper(it).left()) } + .catch { + Timber.tag("UserRepositoryImpl").e(it, "getUsers") + emit(errorMapper(it).left()) + } - override suspend fun refresh() = Either.catch(errorMapper) { + override suspend fun refresh() = Either.catch { getUsersFromRemote().let { changesFlow.emit(Change.Refreshed(it)) } - } + }.tapLeft { Timber.tag("UserRepositoryImpl").e(it, "refresh") } + .mapLeft(errorMapper) - override suspend fun remove(user: User) = Either.catch(errorMapper) { + override suspend fun remove(user: User) = Either.catch { withContext(dispatchers.io) { val response = userApiService.remove(user.id) changesFlow.emit(Change.Removed(responseToDomain(response))) } - } + }.tapLeft { Timber.tag("UserRepositoryImpl").e(it, "remove user=$user") } + .mapLeft(errorMapper) - override suspend fun add(user: User) = Either.catch(errorMapper) { + override suspend fun add(user: User) = Either.catch { withContext(dispatchers.io) { val body = domainToBody(user) val response = userApiService.add(body) changesFlow.emit(Change.Added(responseToDomain(response))) extraDelay() } - } + }.tapLeft { Timber.tag("UserRepositoryImpl").e(it, "add user=$user") } + .mapLeft(errorMapper) - override suspend fun search(query: String) = Either.catch(errorMapper) { + override suspend fun search(query: String) = Either.catch { withContext(dispatchers.io) { extraDelay() userApiService.search(query).map(responseToDomain) } - } + }.tapLeft { Timber.tag("UserRepositoryImpl").e(it, "search query=$query") } + .mapLeft(errorMapper) private suspend inline fun extraDelay() = delay(400) } diff --git a/feature-add/build.gradle.kts b/feature-add/build.gradle.kts index 6a194817..b55d001e 100644 --- a/feature-add/build.gradle.kts +++ b/feature-add/build.gradle.kts @@ -58,6 +58,7 @@ dependencies { implementation(deps.viewBindingDelegate) implementation(deps.flowExt) + implementation(deps.timber) addUnitTest() } diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt index 7ca729a5..91e97ab5 100644 --- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt +++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt @@ -2,7 +2,6 @@ package com.hoc.flowmvi.ui.add import android.content.Context import android.content.Intent -import android.util.Log import android.view.MenuItem import androidx.core.view.isInvisible import androidx.transition.AutoTransition @@ -20,6 +19,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import org.koin.androidx.viewmodel.ext.android.viewModel +import timber.log.Timber @ExperimentalCoroutinesApi class AddActivity : @@ -35,7 +35,7 @@ class AddActivity : } override fun handleSingleEvent(event: SingleEvent) { - Log.d("###", "Event=$event") + Timber.d("Event=$event") return when (event) { is SingleEvent.AddUserSuccess -> { @@ -43,14 +43,13 @@ class AddActivity : finish() } is SingleEvent.AddUserFailure -> { - Log.d("###", event.toString()) toast("Add failure") } } } override fun render(viewState: ViewState) { - Log.d("###", "ViewState=$viewState") + Timber.d("viewState=$viewState") val emailErrorMessage = if (ValidationError.INVALID_EMAIL_ADDRESS in viewState.errors) { "Invalid email" diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt index 67d5616d..f0d5de90 100644 --- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt +++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt @@ -1,6 +1,5 @@ package com.hoc.flowmvi.ui.add -import android.util.Log import androidx.core.util.PatternsCompat import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope @@ -31,6 +30,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.stateIn +import timber.log.Timber @ExperimentalCoroutinesApi class AddVM( @@ -46,13 +46,13 @@ class AddVM( firstName = savedStateHandle.get(FIRST_NAME_KEY), lastName = savedStateHandle.get(LAST_NAME_KEY), ) - Log.d(logTag, "[ADD_VM] initialVS: $initialVS") + Timber.tag(logTag).d("[ADD_VM] initialVS: $initialVS") viewState = intentFlow .toPartialStateChangesFlow() .sendSingleEvent() .scan(initialVS) { state, change -> change.reduce(state) } - .catch { Log.d(logTag, "[ADD_VM] Throwable: $it") } + .catch { Timber.tag(logTag).e(it, "[ADD_VM] Throwable: $it") } .stateIn(viewModelScope, SharingStarted.Eagerly, initialVS) } diff --git a/feature-main/build.gradle.kts b/feature-main/build.gradle.kts index 3bce8ff3..2532aed4 100644 --- a/feature-main/build.gradle.kts +++ b/feature-main/build.gradle.kts @@ -67,6 +67,7 @@ dependencies { implementation(deps.viewBindingDelegate) implementation(deps.flowExt) implementation(deps.arrow.core) + implementation(deps.timber) addUnitTest() testImplementation(mviTesting) diff --git a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt index 2b75a8a8..6f1cd622 100644 --- a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt +++ b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt @@ -1,6 +1,5 @@ package com.hoc.flowmvi.ui.main -import android.util.Log import android.view.Menu import android.view.MenuItem import androidx.core.view.isVisible @@ -27,6 +26,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel +import timber.log.Timber @FlowPreview @ExperimentalCoroutinesApi @@ -81,7 +81,7 @@ class MainActivity : ) override fun handleSingleEvent(event: SingleEvent) { - Log.d("MainActivity", "handleSingleEvent $event") + Timber.d("handleSingleEvent $event") return when (event) { SingleEvent.Refresh.Success -> toast("Refresh success") is SingleEvent.Refresh.Failure -> toast("Refresh failure") @@ -92,7 +92,7 @@ class MainActivity : } override fun render(viewState: ViewState) { - Log.d("MainActivity", "render $viewState") + Timber.d("render $viewState") userAdapter.submitList(viewState.userItems) diff --git a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt index e119906c..d5c07caf 100644 --- a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt +++ b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt @@ -1,6 +1,5 @@ package com.hoc.flowmvi.ui.main -import android.util.Log import androidx.lifecycle.viewModelScope import com.hoc.flowmvi.domain.usecase.GetUsersUseCase import com.hoc.flowmvi.domain.usecase.RefreshGetUsersUseCase @@ -28,6 +27,7 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.take +import timber.log.Timber @FlowPreview @ExperimentalCoroutinesApi @@ -49,7 +49,7 @@ class MainVM( .toPartialChangeFlow() .sendSingleEvent() .scan(initialVS) { vs, change -> change.reduce(vs) } - .catch { Log.d("###", "[MAIN_VM] Throwable: $it") } + .catch { Timber.tag(logTag).e(it, "[MAIN_VM] Throwable: $it") } .stateIn( viewModelScope, SharingStarted.Eagerly, @@ -79,7 +79,7 @@ class MainVM( private fun Flow.toPartialChangeFlow(): Flow = shareWhileSubscribed().run { val getUserChanges = defer(getUsersUseCase::invoke) - .onEach { either -> Log.d("###", "[MAIN_VM] Emit users.size=${either.map { it.size }}") } + .onEach { either -> Timber.d("[MAIN_VM] Emit users.size=${either.map { it.size }}") } .map { result -> result.fold( ifLeft = { PartialChange.GetUser.Error(it) }, diff --git a/feature-search/build.gradle.kts b/feature-search/build.gradle.kts index ddcc0ea9..0b0b5c7e 100644 --- a/feature-search/build.gradle.kts +++ b/feature-search/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { implementation(deps.viewBindingDelegate) implementation(deps.flowExt) implementation(deps.arrow.core) + implementation(deps.timber) addUnitTest() } diff --git a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchActivity.kt b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchActivity.kt index 8b35599b..e0908242 100644 --- a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchActivity.kt +++ b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchActivity.kt @@ -3,7 +3,6 @@ package com.hoc.flowmvi.ui.search import android.content.Context import android.content.Intent import android.content.res.Configuration -import android.util.Log import android.view.Menu import android.view.MenuItem import androidx.appcompat.widget.SearchView @@ -30,6 +29,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import org.koin.androidx.viewmodel.ext.android.viewModel +import timber.log.Timber import kotlin.time.ExperimentalTime @ExperimentalCoroutinesApi @@ -79,7 +79,7 @@ class SearchActivity : override fun viewIntents(): Flow = merge( searchViewQueryTextEventChannel .consumeAsFlow() - .onEach { Log.d("SearchActivity", "Query $it") } + .onEach { Timber.d("Query $it") } .map { ViewIntent.Search(it.query.toString()) }, binding.retryButton.clicks().map { ViewIntent.Retry }, ) diff --git a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchVM.kt b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchVM.kt index 278f7955..ed66e68a 100644 --- a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchVM.kt +++ b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchVM.kt @@ -1,6 +1,5 @@ package com.hoc.flowmvi.ui.search -import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.hoc.flowmvi.domain.usecase.SearchUsersUseCase @@ -27,6 +26,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.stateIn +import timber.log.Timber import kotlin.time.Duration import kotlin.time.ExperimentalTime @@ -49,7 +49,7 @@ class SearchVM( .toPartialStateChangesFlow() .sendSingleEvent() .scan(initialVS) { state, change -> change.reduce(state) } - .catch { Log.d(logTag, "[SEARCH_VM] Throwable: $it") } + .catch { Timber.tag(logTag).e(it, "[SEARCH_VM] Throwable: $it") } .stateIn(viewModelScope, SharingStarted.Eagerly, initialVS) } diff --git a/mvi/mvi-base/build.gradle.kts b/mvi/mvi-base/build.gradle.kts index fe6f9c55..8773f946 100644 --- a/mvi/mvi-base/build.gradle.kts +++ b/mvi/mvi-base/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(deps.coroutines.core) implementation(core) + implementation(deps.timber) addUnitTest() } diff --git a/mvi/mvi-base/src/main/java/com/hoc/flowmvi/mvi_base/AbstractMviViewModel.kt b/mvi/mvi-base/src/main/java/com/hoc/flowmvi/mvi_base/AbstractMviViewModel.kt index c47af2bd..caef665b 100644 --- a/mvi/mvi-base/src/main/java/com/hoc/flowmvi/mvi_base/AbstractMviViewModel.kt +++ b/mvi/mvi-base/src/main/java/com/hoc/flowmvi/mvi_base/AbstractMviViewModel.kt @@ -1,6 +1,6 @@ package com.hoc.flowmvi.mvi_base -import android.util.Log +import android.os.Build import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.channels.Channel @@ -13,11 +13,19 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn +import timber.log.Timber abstract class AbstractMviViewModel : MviViewModel, ViewModel() { protected val logTag by lazy(LazyThreadSafetyMode.PUBLICATION) { - this::class.java.simpleName.take(23) + this::class.java.simpleName.let { tag -> + // Tag length limit was removed in API 26. + if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) { + tag + } else { + tag.take(MAX_TAG_LENGTH) + } + } } private val eventChannel = Channel(Channel.UNLIMITED) @@ -34,7 +42,7 @@ abstract class AbstractMviViewModel Flow.log(subject: String): Flow = - onEach { Log.d(logTag, ">>> $subject: $it") } + onEach { Timber.tag(logTag).d(">>> $subject: $it") } protected fun Flow.shareWhileSubscribed(): SharedFlow = shareIn(viewModelScope, SharingStarted.WhileSubscribed()) @@ -52,5 +60,7 @@ abstract class AbstractMviViewModel