diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
index 6e6eec11..a55e7a17 100644
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -1,6 +1,5 @@
-
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index ba5aac46..52ab6183 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -38,41 +38,41 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
- implementation 'androidx.appcompat:appcompat:1.1.0'
- implementation 'androidx.core:core-ktx:1.2.0'
+ implementation 'androidx.appcompat:appcompat:1.2.0-beta01'
+ implementation 'androidx.core:core-ktx:1.3.0-beta01'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
- implementation 'androidx.recyclerview:recyclerview:1.1.0'
- implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03'
- implementation 'com.google.android.material:material:1.2.0-alpha04'
+ implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha02'
+ implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-beta01'
+ implementation 'com.google.android.material:material:1.2.0-alpha05'
// viewModelScope
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-alpha01'
// lifecycleScope
- implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha01'
// Extensions for LiveData
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-alpha01'
// retrofit2
- implementation 'com.squareup.retrofit2:retrofit:2.7.1'
- implementation 'com.squareup.retrofit2:converter-moshi:2.7.1'
- implementation 'com.squareup.okhttp3:logging-interceptor:4.2.1'
+ implementation 'com.squareup.retrofit2:retrofit:2.8.1'
+ implementation 'com.squareup.retrofit2:converter-moshi:2.8.1'
+ implementation 'com.squareup.okhttp3:logging-interceptor:4.4.0'
// moshi
- implementation 'com.squareup.moshi:moshi-kotlin:1.8.0'
+ implementation 'com.squareup.moshi:moshi-kotlin:1.9.2'
// coroutines
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5'
// koin
- implementation 'org.koin:koin-androidx-viewmodel:2.0.0'
+ implementation 'org.koin:koin-androidx-viewmodel:2.1.5'
// coil
- implementation 'io.coil-kt:coil:0.8.0'
+ implementation 'io.coil-kt:coil:0.9.5'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
diff --git a/app/src/main/java/com/hoc/flowmvi/FlowBinding+Exts+Utils.kt b/app/src/main/java/com/hoc/flowmvi/FlowBinding+Exts+Utils.kt
index e0b7fd42..b97ae274 100644
--- a/app/src/main/java/com/hoc/flowmvi/FlowBinding+Exts+Utils.kt
+++ b/app/src/main/java/com/hoc/flowmvi/FlowBinding+Exts+Utils.kt
@@ -39,8 +39,9 @@ fun View.clicks(): Flow {
}
@ExperimentalCoroutinesApi
+@CheckResult
fun EditText.textChanges(): Flow {
- return callbackFlow {
+ return callbackFlow {
val listener = object : TextWatcher {
override fun afterTextChanged(s: Editable?) = Unit
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
@@ -55,7 +56,7 @@ fun EditText.textChanges(): Flow {
@ExperimentalCoroutinesApi
fun Flow.flatMapFirst(transform: suspend (value: T) -> Flow): Flow =
- map(transform).flattenFirst()
+ map(transform).flattenFirst()
@ExperimentalCoroutinesApi
fun Flow>.flattenFirst(): Flow = channelFlow {
@@ -105,47 +106,48 @@ fun Flow.withLatestFrom(other: Flow, transform: suspend (A, B) -
fun Context.toast(text: CharSequence) = Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
+@ExperimentalCoroutinesApi
suspend fun main() {
(1..2000).asFlow()
- .onEach { delay(50) }
- .flatMapFirst { v ->
- flow {
- delay(500)
- emit(v)
+ .onEach { delay(50) }
+ .flatMapFirst { v ->
+ flow {
+ delay(500)
+ emit(v)
+ }
}
- }
- .onEach { println("[*] $it") }
- .catch { println("Error $it") }
- .collect()
+ .onEach { println("[*] $it") }
+ .catch { println("Error $it") }
+ .collect()
}
class SwipeLeftToDeleteCallback(context: Context, private val onSwipedCallback: (Int) -> Unit) :
- ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
+ ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
private val background: ColorDrawable = ColorDrawable(Color.parseColor("#f44336"))
private val iconDelete =
- ContextCompat.getDrawable(context, R.drawable.ic_baseline_delete_white_24)!!
+ ContextCompat.getDrawable(context, R.drawable.ic_baseline_delete_white_24)!!
override fun onMove(
- recyclerView: RecyclerView,
- viewHolder: RecyclerView.ViewHolder,
- target: RecyclerView.ViewHolder
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder,
+ target: RecyclerView.ViewHolder
) = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
- val position = viewHolder.adapterPosition
+ val position = viewHolder.bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
onSwipedCallback(position)
}
}
override fun onChildDraw(
- c: Canvas,
- recyclerView: RecyclerView,
- viewHolder: RecyclerView.ViewHolder,
- dX: Float,
- dY: Float,
- actionState: Int,
- isCurrentlyActive: Boolean
+ c: Canvas,
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder,
+ dX: Float,
+ dY: Float,
+ actionState: Int,
+ isCurrentlyActive: Boolean
) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
val itemView = viewHolder.itemView
@@ -161,10 +163,10 @@ class SwipeLeftToDeleteCallback(context: Context, private val onSwipedCallback:
iconDelete.setBounds(iconLeft, iconTop, iconRight, iconBottom)
background.setBounds(
- itemView.right + dX.toInt() - 8,
- itemView.top,
- itemView.right,
- itemView.bottom
+ itemView.right + dX.toInt() - 8,
+ itemView.top,
+ itemView.right,
+ itemView.bottom
)
}
else -> background.setBounds(0, 0, 0, 0)
diff --git a/app/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt b/app/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt
index 4c54f1c4..36965289 100644
--- a/app/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt
+++ b/app/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt
@@ -15,7 +15,6 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
-@FlowPreview
@ExperimentalCoroutinesApi
class UserRepositoryImpl(
private val userApiService: UserApiService,
@@ -39,6 +38,7 @@ class UserRepositoryImpl(
}
}
+ @FlowPreview
override fun getUsers(): Flow> {
return flow {
val initial = getUsersFromRemote()
diff --git a/app/src/main/java/com/hoc/flowmvi/koin/DomainModule.kt b/app/src/main/java/com/hoc/flowmvi/koin/DomainModule.kt
index 9fde27f9..6b89b253 100644
--- a/app/src/main/java/com/hoc/flowmvi/koin/DomainModule.kt
+++ b/app/src/main/java/com/hoc/flowmvi/koin/DomainModule.kt
@@ -18,23 +18,23 @@ import org.koin.dsl.module
@ExperimentalCoroutinesApi
@FlowPreview
val domainModule = module {
- single { CoroutineDispatchersImpl() as CoroutineDispatchers }
+ single { CoroutineDispatchersImpl() }
- single {
+ single {
UserRepositoryImpl(
get(),
get(),
responseToDomain = get(),
domainToResponse = get(),
domainToBody = get()
- ) as UserRepository
+ )
}
- single { GetUsersUseCase(get()) }
+ factory { GetUsersUseCase(get()) }
- single { RefreshGetUsersUseCase(get()) }
+ factory { RefreshGetUsersUseCase(get()) }
- single { RemoveUserUseCase(get()) }
+ factory { RemoveUserUseCase(get()) }
- single { AddUserUseCase(get()) }
+ factory { AddUserUseCase(get()) }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt b/app/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt
index 5c29b0d1..d707bd72 100644
--- a/app/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt
+++ b/app/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt
@@ -43,14 +43,14 @@ class AddActivity : AppCompatActivity(), View {
// observe view model
addVM.viewState.observe(this, Observer { render(it ?: return@Observer) })
addVM.singleEvent.observe(
- this,
- Observer { handleSingleEvent(it?.getContentIfNotHandled() ?: return@Observer) }
+ this,
+ Observer { handleSingleEvent(it?.getContentIfNotHandled() ?: return@Observer) }
)
// pass view intent to view model
intents()
- .onEach { addVM.processIntent(it) }
- .launchIn(lifecycleScope)
+ .onEach { addVM.processIntent(it) }
+ .launchIn(lifecycleScope)
}
private fun handleSingleEvent(event: SingleEvent) {
@@ -102,25 +102,25 @@ class AddActivity : AppCompatActivity(), View {
override fun intents(): Flow {
return merge(
- addBinding
- .emailEditText
- .editText!!
- .textChanges()
- .map { ViewIntent.EmailChanged(it?.toString()) },
- addBinding
- .firstNameEditText
- .editText!!
- .textChanges()
- .map { ViewIntent.FirstNameChanged(it?.toString()) },
- addBinding
- .lastNameEditText
- .editText!!
- .textChanges()
- .map { ViewIntent.LastNameChanged(it?.toString()) },
- addBinding
- .addButton
- .clicks()
- .map { ViewIntent.Submit }
+ addBinding
+ .emailEditText
+ .editText!!
+ .textChanges()
+ .map { ViewIntent.EmailChanged(it?.toString()) },
+ addBinding
+ .firstNameEditText
+ .editText!!
+ .textChanges()
+ .map { ViewIntent.FirstNameChanged(it?.toString()) },
+ addBinding
+ .lastNameEditText
+ .editText!!
+ .textChanges()
+ .map { ViewIntent.LastNameChanged(it?.toString()) },
+ addBinding
+ .addButton
+ .clicks()
+ .map { ViewIntent.Submit }
)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt b/app/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt
index 40863de9..3569ed61 100644
--- a/app/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt
+++ b/app/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt
@@ -15,13 +15,13 @@ interface AddContract {
}
data class ViewState(
- val errors: Set,
- val isLoading: Boolean
+ val errors: Set,
+ val isLoading: Boolean
) {
companion object {
fun initial() = ViewState(
- errors = emptySet(),
- isLoading = false
+ errors = emptySet(),
+ isLoading = false
)
}
}
diff --git a/app/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt b/app/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt
index 1a89c2b9..546dd046 100644
--- a/app/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt
+++ b/app/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt
@@ -30,75 +30,78 @@ class AddVM(private val addUser: AddUserUseCase) : ViewModel() {
init {
_intentChannel
- .asFlow()
- .toPartialStateChangesFlow()
- .sendSingleEvent()
- .scan(initialVS) { state, change -> change.reduce(state) }
- .onEach { _viewStateD.value = it }
- .catch { }
- .launchIn(viewModelScope)
+ .asFlow()
+ .toPartialStateChangesFlow()
+ .sendSingleEvent()
+ .scan(initialVS) { state, change -> change.reduce(state) }
+ .onEach { _viewStateD.value = it }
+ .catch { }
+ .launchIn(viewModelScope)
}
private fun Flow.sendSingleEvent(): Flow {
return onEach { change ->
_eventD.value = Event(
- when (change) {
- is PartialStateChange.ErrorsChanged -> return@onEach
- PartialStateChange.AddUser.Loading -> return@onEach
- is PartialStateChange.AddUser.AddUserSuccess -> SingleEvent.AddUserSuccess(change.user)
- is PartialStateChange.AddUser.AddUserFailure -> SingleEvent.AddUserFailure(
- change.user,
- change.throwable
- )
- }
+ when (change) {
+ is PartialStateChange.ErrorsChanged -> return@onEach
+ PartialStateChange.AddUser.Loading -> return@onEach
+ is PartialStateChange.AddUser.AddUserSuccess -> SingleEvent.AddUserSuccess(change.user)
+ is PartialStateChange.AddUser.AddUserFailure -> SingleEvent.AddUserFailure(
+ change.user,
+ change.throwable
+ )
+ }
)
}
}
private fun Flow.toPartialStateChangesFlow(): Flow {
val emailErrors = filterIsInstance()
- .map { it.email }
- .map { validateEmail(it) to it }
+ .map { it.email }
+ .map { validateEmail(it) to it }
val firstNameErrors = filterIsInstance()
- .map { it.firstName }
- .map { validateFirstName(it) to it }
+ .map { it.firstName }
+ .map { validateFirstName(it) to it }
val lastNameErrors = filterIsInstance()
- .map { it.lastName }
- .map { validateLastName(it) to it }
+ .map { it.lastName }
+ .map { validateLastName(it) to it }
val userFormFlow =
- combine(emailErrors, firstNameErrors, lastNameErrors) { email, firstName, lastName ->
- UserForm(
- errors = email.first + firstName.first + lastName.first,
- user = User(
- firstName = firstName.second ?: "",
- email = email.second ?: "",
- lastName = lastName.second ?: "",
- id = "",
- avatar = ""
+ combine(emailErrors, firstNameErrors, lastNameErrors) { email, firstName, lastName ->
+ UserForm(
+ errors = email.first + firstName.first + lastName.first,
+ user = User(
+ firstName = firstName.second ?: "",
+ email = email.second ?: "",
+ lastName = lastName.second ?: "",
+ id = "",
+ avatar = ""
+ )
)
- )
- }
+ }
val addUserChanges = filterIsInstance()
- .withLatestFrom(userFormFlow) { _, userForm -> userForm }
- .filter { it.errors.isEmpty() }
- .map { it.user }
- .flatMapFirst { user ->
- flow { emit(addUser(user)) }
- .map { PartialStateChange.AddUser.AddUserSuccess(user) as PartialStateChange.AddUser }
- .onStart { emit(PartialStateChange.AddUser.Loading) }
- .catch { emit(PartialStateChange.AddUser.AddUserFailure(user, it)) }
- }
+ .withLatestFrom(userFormFlow) { _, userForm -> userForm }
+ .filter { it.errors.isEmpty() }
+ .map { it.user }
+ .flatMapFirst { user ->
+ flow { emit(addUser(user)) }
+ .map {
+ @Suppress("USELESS_CAST")
+ PartialStateChange.AddUser.AddUserSuccess(user) as PartialStateChange.AddUser
+ }
+ .onStart { emit(PartialStateChange.AddUser.Loading) }
+ .catch { emit(PartialStateChange.AddUser.AddUserFailure(user, it)) }
+ }
return merge(
- userFormFlow
- .map { it.errors }
- .map { PartialStateChange.ErrorsChanged(it) },
- addUserChanges
+ userFormFlow
+ .map { it.errors }
+ .map { PartialStateChange.ErrorsChanged(it) },
+ addUserChanges
)
}
@@ -107,8 +110,8 @@ class AddVM(private val addUser: AddUserUseCase) : ViewModel() {
const val MIN_LENGTH_LAST_NAME = 3
private data class UserForm(
- val errors: Set,
- val user: User
+ val errors: Set,
+ val user: User
)
fun validateFirstName(firstName: String?): Set {
diff --git a/app/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt b/app/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt
index 87471c7d..c33abd95 100644
--- a/app/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt
+++ b/app/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt
@@ -72,13 +72,17 @@ class MainVM(
.onEach { Log.d("###", "[MAIN_VM] Emit users.size=${it.size}") }
.map {
val items = it.map(::UserItem)
+ @Suppress("USELESS_CAST")
PartialChange.GetUser.Data(items) as PartialChange.GetUser
}
.onStart { emit(PartialChange.GetUser.Loading) }
.catch { emit(PartialChange.GetUser.Error(it)) }
val refreshChanges = flow { emit(refreshGetUsersUseCase()) }
- .map { PartialChange.Refresh.Success as PartialChange.Refresh }
+ .map {
+ @Suppress("USELESS_CAST")
+ PartialChange.Refresh.Success as PartialChange.Refresh
+ }
.onStart { emit(PartialChange.Refresh.Loading) }
.catch { emit(PartialChange.Refresh.Failure(it)) }
diff --git a/build.gradle b/build.gradle
index e58081e2..cf7a39c3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,13 +1,14 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '1.3.61'
+ ext.kotlin_version = '1.4-M1'
repositories {
google()
jcenter()
+ maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.6.0-rc02'
+ classpath 'com.android.tools.build:gradle:3.6.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
@@ -20,6 +21,7 @@ allprojects {
google()
jcenter()
mavenCentral()
+ maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
}
}