diff --git a/.github/workflows/qodana.yml b/.github/workflows/qodana.yml index bef0f956..98919475 100644 --- a/.github/workflows/qodana.yml +++ b/.github/workflows/qodana.yml @@ -1,21 +1,21 @@ -name: Qodana - -on: - push: - branches: [ master ] - paths-ignore: [ '**.md', '**.MD' ] - pull_request: - branches: [ master ] - paths-ignore: [ '**.md', '**.MD' ] - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - uses: JetBrains/qodana-action@v4.2.3 - with: - linter: jetbrains/qodana-jvm-android:latest - fail-threshold: 10 +name: Qodana + +on: + push: + branches: [ master ] + paths-ignore: [ '**.md', '**.MD' ] + pull_request: + branches: [ master ] + paths-ignore: [ '**.md', '**.MD' ] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: JetBrains/qodana-action@v4.2.3 + with: + linter: jetbrains/qodana-jvm-android:latest + fail-threshold: 10 diff --git a/.gitignore b/.gitignore index a26b43b5..b10b0d06 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,8 @@ /.idea/assetWizardSettings.xml .DS_Store /build -buildSrc/build /captures .externalNativeBuild .cxx +local.properties +**/build/ diff --git a/.idea/misc.xml b/.idea/misc.xml index 67a156c7..bd2b2cb7 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -14,11 +14,11 @@ - + - - + + diff --git a/app/release/app-release.apk b/app/release/app-release.apk deleted file mode 100644 index 090c3d46..00000000 Binary files a/app/release/app-release.apk and /dev/null differ diff --git a/app/src/main/java/com/hoc/flowmvi/core/CoreModule.kt b/app/src/main/java/com/hoc/flowmvi/core/CoreModule.kt index bacf7866..93ac93a3 100644 --- a/app/src/main/java/com/hoc/flowmvi/core/CoreModule.kt +++ b/app/src/main/java/com/hoc/flowmvi/core/CoreModule.kt @@ -4,6 +4,7 @@ import com.hoc.flowmvi.core.dispatchers.CoroutineDispatchers import com.hoc.flowmvi.core_ui.navigator.Navigator import org.koin.dsl.module +@JvmField val coreModule = module { single { DefaultCoroutineDispatchers() } diff --git a/buildSrc/.gitignore b/buildSrc/.gitignore new file mode 100644 index 00000000..ca730c42 --- /dev/null +++ b/buildSrc/.gitignore @@ -0,0 +1,2 @@ +/build +.gradle diff --git a/core-ui/src/main/AndroidManifest.xml b/core-ui/src/main/AndroidManifest.xml index 069f7323..6ae8afba 100644 --- a/core-ui/src/main/AndroidManifest.xml +++ b/core-ui/src/main/AndroidManifest.xml @@ -1,13 +1,13 @@ + package="com.hoc.flowmvi.core_ui"> - + - + - + diff --git a/core-ui/src/main/java/com/hoc/flowmvi/core_ui/FlowBinding.kt b/core-ui/src/main/java/com/hoc/flowmvi/core_ui/FlowBinding.kt index 105f0069..83d357ae 100644 --- a/core-ui/src/main/java/com/hoc/flowmvi/core_ui/FlowBinding.kt +++ b/core-ui/src/main/java/com/hoc/flowmvi/core_ui/FlowBinding.kt @@ -8,7 +8,6 @@ import androidx.appcompat.widget.SearchView import androidx.core.widget.doOnTextChanged import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -23,7 +22,6 @@ internal fun checkMainThread() { } } -@ExperimentalCoroutinesApi @CheckResult fun EditText.firstChange(): Flow { return callbackFlow { @@ -39,7 +37,6 @@ fun EditText.firstChange(): Flow { }.take(1) } -@ExperimentalCoroutinesApi @CheckResult fun SwipeRefreshLayout.refreshes(): Flow { return callbackFlow { @@ -50,7 +47,6 @@ fun SwipeRefreshLayout.refreshes(): Flow { } } -@ExperimentalCoroutinesApi @CheckResult fun View.clicks(): Flow { return callbackFlow { @@ -67,7 +63,6 @@ data class SearchViewQueryTextEvent( val isSubmitted: Boolean, ) -@ExperimentalCoroutinesApi @CheckResult fun SearchView.queryTextEvents(): Flow { return callbackFlow { @@ -109,7 +104,6 @@ fun SearchView.queryTextEvents(): Flow { } } -@ExperimentalCoroutinesApi @CheckResult fun EditText.textChanges(): Flow { return callbackFlow { diff --git a/core-ui/src/main/res/font/noto_sans.xml b/core-ui/src/main/res/font/noto_sans.xml index 6cbfda08..50d1747e 100644 --- a/core-ui/src/main/res/font/noto_sans.xml +++ b/core-ui/src/main/res/font/noto_sans.xml @@ -1,7 +1,6 @@ - + app:fontProviderAuthority="com.google.android.gms.fonts" + app:fontProviderCerts="@array/com_google_android_gms_fonts_certs" + app:fontProviderPackage="com.google.android.gms" + app:fontProviderQuery="Noto Sans"> diff --git a/core-ui/src/main/res/values/themes.xml b/core-ui/src/main/res/values/themes.xml index 01983e75..e010bd31 100644 --- a/core-ui/src/main/res/values/themes.xml +++ b/core-ui/src/main/res/values/themes.xml @@ -1,30 +1,31 @@ - + + diff --git a/core/src/main/java/com/hoc/flowmvi/core/SuspendRetry.kt b/core/src/main/java/com/hoc/flowmvi/core/SuspendRetry.kt deleted file mode 100644 index ab55a013..00000000 --- a/core/src/main/java/com/hoc/flowmvi/core/SuspendRetry.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.hoc.flowmvi.core - -import kotlinx.coroutines.delay -import kotlin.time.Duration -import kotlin.time.ExperimentalTime - -@ExperimentalTime -suspend inline fun retrySuspend( - times: Int, - initialDelay: Duration, - factor: Double, - maxDelay: Duration = Duration.INFINITE, - shouldRetry: (Throwable) -> Boolean = { true }, - block: (times: Int) -> T, -): T { - var currentDelay = initialDelay - repeat(times - 1) { - try { - return block(it) - } catch (e: Throwable) { - if (!shouldRetry(e)) { - throw e - } - // you can log an error here and/or make a more finer-grained - // analysis of the cause to see if retry is needed - } - delay(currentDelay) - currentDelay = (currentDelay * factor).coerceAtMost(maxDelay) - } - return block(times - 1) // last attempt -} diff --git a/data/build.gradle.kts b/data/build.gradle.kts index f1dc6ff9..a70eba5f 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(domain) implementation(deps.coroutines.core) + implementation(deps.flowExt) implementation(deps.squareup.retrofit) implementation(deps.squareup.moshiKotlin) diff --git a/data/src/main/AndroidManifest.xml b/data/src/main/AndroidManifest.xml index fe5edcd8..72621c55 100644 --- a/data/src/main/AndroidManifest.xml +++ b/data/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/data/src/main/java/com/hoc/flowmvi/data/DataModule.kt b/data/src/main/java/com/hoc/flowmvi/data/DataModule.kt index 710fac77..45ade5ef 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/DataModule.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/DataModule.kt @@ -10,6 +10,7 @@ import com.squareup.moshi.Moshi import com.squareup.moshi.adapter import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor.Level @@ -22,6 +23,8 @@ import kotlin.time.ExperimentalTime val BASE_URL_QUALIFIER = named("BASE_URL") +@JvmField +@FlowPreview @ExperimentalStdlibApi @ExperimentalTime @ExperimentalCoroutinesApi 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 ae3de8d9..ac7022ae 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt @@ -8,7 +8,6 @@ import arrow.core.right import arrow.core.valueOr import com.hoc.flowmvi.core.Mapper import com.hoc.flowmvi.core.dispatchers.CoroutineDispatchers -import com.hoc.flowmvi.core.retrySuspend import com.hoc.flowmvi.data.remote.UserApiService import com.hoc.flowmvi.data.remote.UserBody import com.hoc.flowmvi.data.remote.UserResponse @@ -16,12 +15,15 @@ import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.domain.model.UserValidationError import com.hoc.flowmvi.domain.repository.UserRepository +import com.hoc081098.flowext.retryWithExponentialBackoff import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.scan @@ -32,6 +34,7 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.ExperimentalTime import arrow.core.Either.Companion.catch as catchEither +@FlowPreview @ExperimentalTime @ExperimentalCoroutinesApi internal class UserRepositoryImpl( @@ -65,44 +68,38 @@ internal class UserRepositoryImpl( @Suppress("NOTHING_TO_INLINE") private inline fun logError(t: Throwable, message: String) = Timber.tag(TAG).e(t, message) - private suspend fun getUsersFromRemote(): List { - return withContext(dispatchers.io) { - retrySuspend( - times = 3, - initialDelay = 500.milliseconds, - factor = 2.0, - shouldRetry = { it is IOException } - ) { times -> - Timber.d("[USER_REPO] Retry times=$times") - userApiService - .getUsers() - .map(responseToDomainThrows) - } - } - } - - override fun getUsers() = flow { - val initial = getUsersFromRemote() - - changesFlow - .onEach { Timber.d("[USER_REPO] Change=$it") } - .scan(initial) { acc, change -> - when (change) { - is Change.Removed -> acc.filter { it.id != change.removed.id } - is Change.Refreshed -> change.user - is Change.Added -> acc + change.user + private fun getUsersFromRemote(): Flow> = suspend { + Timber.d("[USER_REPO] getUsersFromRemote ...") + userApiService + .getUsers() + .map(responseToDomainThrows) + }.asFlow() + .retryWithExponentialBackoff( + maxAttempt = 2, + initialDelay = 500.milliseconds, + factor = 2.0, + ) { it is IOException } + + override fun getUsers() = getUsersFromRemote() + .flatMapConcat { initial -> + changesFlow + .onEach { Timber.d("[USER_REPO] Change=$it") } + .scan(initial) { acc, change -> + when (change) { + is Change.Removed -> acc.filter { it.id != change.removed.id } + is Change.Refreshed -> change.user + is Change.Added -> acc + change.user + } } - } - .onEach { Timber.d("[USER_REPO] Emit users.size=${it.size} ") } - .let { emitAll(it) } - } + } + .onEach { Timber.d("[USER_REPO] Emit users.size=${it.size} ") } .map { it.right().leftWiden>() } .catch { logError(it, "getUsers") emit(errorMapper(it).left()) } - override suspend fun refresh() = catchEither { getUsersFromRemote() } + override suspend fun refresh() = catchEither { getUsersFromRemote().first() } .tap { sendChange(Change.Refreshed(it)) } .map { } .tapLeft { logError(it, "refresh") } @@ -131,8 +128,6 @@ internal class UserRepositoryImpl( .mapLeft(errorMapper) .bind() - delay(400) // TODO - val added = responseToDomain(response) .mapLeft { UserError.ValidationFailed(it.toSet()) } .tapInvalid { logError(it, "add user=$user") } diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt index 7187582a..f465a5dc 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt @@ -26,6 +26,7 @@ import io.mockk.verify import io.mockk.verifySequence import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch @@ -98,6 +99,7 @@ private val USERS = listOf( private val VALID_NEL_USERS = USERS.map(User::validNel) +@FlowPreview @ExperimentalCoroutinesApi @ExperimentalTime class UserRepositoryImplTest { @@ -165,7 +167,7 @@ class UserRepositoryImplTest { assertTrue(result.isLeft()) assertEquals(UserError.NetworkError, result.leftOrThrow) - coVerify(exactly = 3) { userApiService.getUsers() } // retry 3 times + coVerify(exactly = 3) { userApiService.getUsers() } // retry 2 times verify(exactly = 1) { errorMapper(ofType()) } } @@ -315,7 +317,7 @@ class UserRepositoryImplTest { assertNull(result.orNull()) assertEquals(UserError.NetworkError, result.leftOrThrow) - coVerify(exactly = 3) { userApiService.getUsers() } // retry 3 times. + coVerify(exactly = 3) { userApiService.getUsers() } // retry 2 times. verify(exactly = 1) { errorMapper(ofType()) } } diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/DomainModule.kt b/domain/src/main/java/com/hoc/flowmvi/domain/DomainModule.kt index 9f9691a6..ef4f868f 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/DomainModule.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/DomainModule.kt @@ -7,6 +7,7 @@ import com.hoc.flowmvi.domain.usecase.RemoveUserUseCase import com.hoc.flowmvi.domain.usecase.SearchUsersUseCase import org.koin.dsl.module +@JvmField val domainModule = module { factory { GetUsersUseCase(userRepository = get()) } diff --git a/feature-add/src/main/AndroidManifest.xml b/feature-add/src/main/AndroidManifest.xml index de206c62..365cc847 100644 --- a/feature-add/src/main/AndroidManifest.xml +++ b/feature-add/src/main/AndroidManifest.xml @@ -1,13 +1,13 @@ + package="com.hoc.flowmvi.ui.add"> - + - + - + 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 812e5b66..5d13b899 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 @@ -67,6 +67,7 @@ class AddActivity : else null } + TransitionManager.endTransitions(addBinding.root) TransitionManager.beginDelayedTransition( addBinding.root, AutoTransition() diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddModule.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddModule.kt index ca1f178d..184ffe8b 100644 --- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddModule.kt +++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddModule.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module +@JvmField @ExperimentalCoroutinesApi val addModule = module { viewModel { params -> diff --git a/feature-add/src/main/res/layout/activity_add.xml b/feature-add/src/main/res/layout/activity_add.xml index 5a145e88..69174df3 100644 --- a/feature-add/src/main/res/layout/activity_add.xml +++ b/feature-add/src/main/res/layout/activity_add.xml @@ -48,8 +48,7 @@ android:layout_marginEnd="16dp" android:fontFamily="@font/noto_sans" android:inputType="textEmailAddress" - android:singleLine="true" - android:textSize="16sp" /> + android:singleLine="true" /> + android:singleLine="true" /> + android:singleLine="true" /> - + package="com.hoc.flowmvi.ui.main"> - + - - - + + + - - - + + + - + diff --git a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainModule.kt b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainModule.kt index 95c1ec18..6d91c7cb 100644 --- a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainModule.kt +++ b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainModule.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.FlowPreview import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module +@JvmField @ExperimentalCoroutinesApi @FlowPreview val mainModule = module { diff --git a/feature-main/src/main/res/drawable/ic_baseline_search_24.xml b/feature-main/src/main/res/drawable/ic_baseline_search_24.xml index 07b76d62..cbf0cc71 100644 --- a/feature-main/src/main/res/drawable/ic_baseline_search_24.xml +++ b/feature-main/src/main/res/drawable/ic_baseline_search_24.xml @@ -1,10 +1,10 @@ - + android:viewportHeight="24"> + diff --git a/feature-main/src/main/res/layout/activity_main.xml b/feature-main/src/main/res/layout/activity_main.xml index 5cf781b9..83ae9793 100644 --- a/feature-main/src/main/res/layout/activity_main.xml +++ b/feature-main/src/main/res/layout/activity_main.xml @@ -22,23 +22,22 @@ - - - + android:layout_height="wrap_content"> - - - - + diff --git a/feature-search/src/main/AndroidManifest.xml b/feature-search/src/main/AndroidManifest.xml index c7eee54e..2e174979 100644 --- a/feature-search/src/main/AndroidManifest.xml +++ b/feature-search/src/main/AndroidManifest.xml @@ -1,11 +1,11 @@ + package="com.hoc.flowmvi.ui.search"> - - - + + + 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 a5559176..7579065e 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 @@ -10,6 +10,8 @@ import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager +import androidx.transition.AutoTransition +import androidx.transition.TransitionManager import com.hoc.flowmvi.core_ui.SearchViewQueryTextEvent import com.hoc.flowmvi.core_ui.clicks import com.hoc.flowmvi.core_ui.navigator.IntentProviders @@ -58,6 +60,15 @@ class SearchActivity : textQuery.text = "Search results for '${viewState.submittedQuery}'" } + TransitionManager.endTransitions(root) + TransitionManager.beginDelayedTransition( + root, + AutoTransition() + .addTarget(errorGroup) + .addTarget(progressBar) + .setDuration(200) + ) + errorGroup.isVisible = viewState.error !== null if (errorGroup.isVisible) { errorMessageTextView.text = viewState.error?.let { diff --git a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchContract.kt b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchContract.kt index 48c01a30..aa6e0af3 100644 --- a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchContract.kt +++ b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchContract.kt @@ -21,7 +21,7 @@ data class UserItem private constructor( id = domain.id, email = domain.email.value, avatar = domain.avatar, - fullName = "${domain.firstName} ${domain.lastName}", + fullName = "${domain.firstName.value} ${domain.lastName.value}", ) } } diff --git a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchModule.kt b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchModule.kt index 64895147..e52baf28 100644 --- a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchModule.kt +++ b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchModule.kt @@ -7,6 +7,7 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module import kotlin.time.ExperimentalTime +@JvmField @ExperimentalCoroutinesApi @FlowPreview @ExperimentalTime diff --git a/feature-search/src/main/res/drawable/ic_baseline_search_24.xml b/feature-search/src/main/res/drawable/ic_baseline_search_24.xml index 07b76d62..cbf0cc71 100644 --- a/feature-search/src/main/res/drawable/ic_baseline_search_24.xml +++ b/feature-search/src/main/res/drawable/ic_baseline_search_24.xml @@ -1,10 +1,10 @@ - + android:viewportHeight="24"> + diff --git a/feature-search/src/main/res/layout/activity_search.xml b/feature-search/src/main/res/layout/activity_search.xml index 1b23af7e..339ac6b5 100644 --- a/feature-search/src/main/res/layout/activity_search.xml +++ b/feature-search/src/main/res/layout/activity_search.xml @@ -1,94 +1,91 @@ - - + android:layout_height="match_parent"> - + + + - + - + - + - + diff --git a/feature-search/src/main/res/layout/item_recycler_search_user.xml b/feature-search/src/main/res/layout/item_recycler_search_user.xml index 41fa3a90..c4bacf5c 100644 --- a/feature-search/src/main/res/layout/item_recycler_search_user.xml +++ b/feature-search/src/main/res/layout/item_recycler_search_user.xml @@ -1,62 +1,62 @@ - - - - + android:background="?attr/colorSurface" + tools:layout_width="150dp"> - + + + + + diff --git a/feature-search/src/main/res/menu/menu_search.xml b/feature-search/src/main/res/menu/menu_search.xml index 30027c2d..157ee94c 100644 --- a/feature-search/src/main/res/menu/menu_search.xml +++ b/feature-search/src/main/res/menu/menu_search.xml @@ -1,11 +1,11 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> - + diff --git a/feature-search/src/test/java/com/hoc/flowmvi/ui/search/SearchContractTest.kt b/feature-search/src/test/java/com/hoc/flowmvi/ui/search/SearchContractTest.kt new file mode 100644 index 00000000..907091c9 --- /dev/null +++ b/feature-search/src/test/java/com/hoc/flowmvi/ui/search/SearchContractTest.kt @@ -0,0 +1,49 @@ +package com.hoc.flowmvi.ui.search + +import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.test_utils.valueOrThrow +import kotlin.test.Test +import kotlin.test.assertEquals + +class SearchContractTest { + @Test + fun test_userItem_equals() { + assertEquals( + UserItem.from(createUser()), + UserItem.from(createUser()) + ) + } + + @Test + fun test_userItem_hashCode() { + assertEquals( + UserItem.from(createUser()).hashCode(), + UserItem.from(createUser()).hashCode() + ) + } + + @Test + fun test_userItem_properties() { + val item = UserItem.from(createUser()) + assertEquals(ID, item.id) + assertEquals(EMAIL, item.email) + assertEquals(AVATAR, item.avatar) + assertEquals("$FIRST_NAME $LAST_NAME", item.fullName) + } + + private companion object { + private const val ID = "0" + private const val EMAIL = "test@gmail.com" + private const val AVATAR = "avatar.png" + private const val FIRST_NAME = "first" + private const val LAST_NAME = "last" + + private fun createUser(): User = User.create( + id = ID, + email = EMAIL, + avatar = AVATAR, + firstName = FIRST_NAME, + lastName = LAST_NAME + ).valueOrThrow + } +} diff --git a/gradlew.bat b/gradlew.bat index ac1b06f9..107acd32 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mvi/mvi-base/src/main/AndroidManifest.xml b/mvi/mvi-base/src/main/AndroidManifest.xml index e49ccc80..21977049 100644 --- a/mvi/mvi-base/src/main/AndroidManifest.xml +++ b/mvi/mvi-base/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/mvi/mvi-testing/src/main/AndroidManifest.xml b/mvi/mvi-testing/src/main/AndroidManifest.xml index 3a1feaf0..9c3c1498 100644 --- a/mvi/mvi-testing/src/main/AndroidManifest.xml +++ b/mvi/mvi-testing/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - +