Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate From LiveData to Coroutines Flows & Channels #51

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 42 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,53 @@

### API Key

To run the application you need to supply an API key from [TMBD](https://developers.themoviedb.org/3/getting-started/introduction). When you get the key please add following variable to your local environment:
To run the application you need to supply an API key
from [TMBD](https://developers.themoviedb.org/3/getting-started/introduction). When you get the key
please add following variable to your local environment:

`` API_KEY_TMDB = Your API Key ``

How to set an environment variable in [Mac](https://medium.com/@himanshuagarwal1395/setting-up-environment-variables-in-macos-sierra-f5978369b255) / [Windows](https://www.architectryan.com/2018/08/31/how-to-change-environment-variables-on-windows-10/)
How to set an environment variable
in [Mac](https://medium.com/@himanshuagarwal1395/setting-up-environment-variables-in-macos-sierra-f5978369b255) / [Windows](https://www.architectryan.com/2018/08/31/how-to-change-environment-variables-on-windows-10/)

### Code style [*](https://github.com/VMadalin/kotlin-sample-app)

To maintain the style and quality of the code, are used the bellow static analysis tools. All of them use properly configuration and you find them in the project root directory `config/.{toolName}`.
To maintain the style and quality of the code, are used the bellow static analysis tools. All of
them use properly configuration and you find them in the project root
directory `config/.{toolName}`.

| Tools | Config file | Check command | Fix command |
|-----------------------------------|---------------------------------------:|------------------------------|---------------------------|
| [detekt][detekt] | [.detekt.yml](/config/.detekt.yml) | `./gradlew detekt` | - |
| [ktlint][ktlint] | - | `./gradlew ktlint` | `./gradlew ktlintFormat` |
| [spotless][spotless] | - | `./gradlew spotlessCheck` | `./gradlew spotlessApply` |
| [lint][lint] | [.lint.xml](/config/.lint.xml) | `./gradlew lint` | - |
| [gradle versions plugin][gvPlugin]| - | `./gradlew dependencyUpdates`| - |
| Tools | Config file | Check command | Fix command |
|------------------------------------|-----------------------------------:|-------------------------------|---------------------------|
| [detekt][detekt] | [.detekt.yml](/config/.detekt.yml) | `./gradlew detekt` | - |
| [ktlint][ktlint] | - | `./gradlew ktlint` | `./gradlew ktlintFormat` |
| [spotless][spotless] | - | `./gradlew spotlessCheck` | `./gradlew spotlessApply` |
| [lint][lint] | [.lint.xml](/config/.lint.xml) | `./gradlew lint` | - |
| [gradle versions plugin][gvPlugin] | - | `./gradlew dependencyUpdates` | - |

All these tools, except [Gradle Versions Plugin][gvPlugin], are integrated in [pre-commit git hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks), in order
ensure that all static analysis and tests passes before you can commit your changes. [Gradle Versions Plugin][gvPlugin] can be run optionally. To skip them for specific commit add this option at your git command:
All these tools, except [Gradle Versions Plugin][gvPlugin], are integrated
in [pre-commit git hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks), in order
ensure that all static analysis and tests passes before you can commit your
changes. [Gradle Versions Plugin][gvPlugin] can be run optionally. To skip them for specific commit
add this option at your git command:

```properties
git commit --no-verify
```

It's highly recommended to fix broken code styles. There is [a gradle task](/build.gradle#L57) which execute `ktlintFormat` and `spotlessApply` for you:
It's highly recommended to fix broken code styles. There is [a gradle task](/build.gradle#L57) which
execute `ktlintFormat` and `spotlessApply` for you:

```properties
./gradlew reformat
```

The pre-commit git hooks have exactly the same checks as [CircleCI](https://circleci.com/) and are
defined in this [script](/config/scripts/git-hooks/pre-commit.sh). This step ensures that all
commits comply with the established rules. However the continuous integration will ultimately be
validated that the changes are correct.

The pre-commit git hooks have exactly the same checks as [CircleCI](https://circleci.com/) and are defined in this [script](/config/scripts/git-hooks/pre-commit.sh). This step ensures that all commits comply with the established rules. However the continuous integration will ultimately be validated that the changes are correct.


If you want to know more about naming convention, code style and more please look at our [Android guideline](https://github.com/adessoTurkey/android-guideline) repository.
If you want to know more about naming convention, code style and more please look at
our [Android guideline](https://github.com/adessoTurkey/android-guideline) repository.

## Architecture

Expand All @@ -50,7 +61,8 @@ If you want to know more about naming convention, code style and more please loo

**ViewModel:** Can have simple UI logic but most of the time just gets the data from UseCase

**UseCase:** Contains all business rules and they written in the manner of single responsibility principle
**UseCase:** Contains all business rules and they written in the manner of single responsibility
principle

**Repository:** Single source of data. Responsible to get data from one or more data sources

Expand All @@ -60,10 +72,13 @@ If you want to know more about naming convention, code style and more please loo

#### Dependencies

- **[Navigation Component](https://developer.android.com/jetpack/androidx/releases/navigation):** Consistent navigation between views
- **[LiveData](https://developer.android.com/topic/libraries/architecture/livedata):** Lifecycle aware observable and data holder
- **[ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel):** Holds UI data across configuration changes
- **[Databinding](https://developer.android.com/topic/libraries/data-binding/):** Binds UI components in layouts to data sources
- **[Navigation Component](https://developer.android.com/jetpack/androidx/releases/navigation):**
Consistent navigation between views
- **[Flow](https://developer.android.com/kotlin/flow):** Asynchronous data streams
- **[ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel):** Holds UI
data across configuration changes
- **[Databinding](https://developer.android.com/topic/libraries/data-binding/):** Binds UI
components in layouts to data sources
- **[Dagger](https://github.com/google/dagger):** Dependency injector
- **[Coroutines](https://github.com/Kotlin/kotlinx.coroutines):** Asynchronous programming
- **[Glide](https://github.com/bumptech/glide):** Image loading and caching
Expand Down Expand Up @@ -100,7 +115,11 @@ limitations under the License.
```

[detekt]: https://github.com/arturbosch/detekt

[ktlint]: https://github.com/pinterest/ktlint
[spotless]: https://github.com/diffplug/spotless

[spotless]: https://github.com/diffplug/spotless

[lint]: https://developer.android.com/studio/write/lint

[gvPlugin]: https://github.com/ben-manes/gradle-versions-plugin
31 changes: 16 additions & 15 deletions app/src/main/kotlin/com/adesso/movee/base/BaseAndroidViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,32 @@ import android.annotation.SuppressLint
import android.app.Application
import androidx.annotation.StringRes
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavDirections
import com.adesso.movee.R
import com.adesso.movee.internal.popup.PopUpType
import com.adesso.movee.internal.popup.PopupCallback
import com.adesso.movee.internal.popup.PopupUiModel
import com.adesso.movee.internal.util.Event
import com.adesso.movee.internal.util.Failure
import com.adesso.movee.navigation.NavigationCommand
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.withContext

@SuppressLint("StaticFieldLeak")
abstract class BaseAndroidViewModel(application: Application) : AndroidViewModel(application) {

private val _failurePopup = MutableLiveData<Event<PopupUiModel>>()
val failurePopup: LiveData<Event<PopupUiModel>> = _failurePopup
private val _failurePopup = Channel<PopupUiModel>(Channel.CONFLATED)
val failurePopup = _failurePopup.receiveAsFlow()

private val _success = MutableLiveData<Event<String>>()
val success: LiveData<Event<String>> = _success
private val _success = Channel<String>(Channel.CONFLATED)
val success = _success.receiveAsFlow()

private val _navigation = MutableLiveData<Event<NavigationCommand>>()
val navigation: LiveData<Event<NavigationCommand>> = _navigation
private val _navigation = Channel<NavigationCommand>(Channel.CONFLATED)
val navigation = _navigation.receiveAsFlow()

protected open fun handleFailure(failure: Failure) {
val (title, message) = when (failure) {
Expand Down Expand Up @@ -65,7 +66,7 @@ abstract class BaseAndroidViewModel(application: Application) : AndroidViewModel
else -> Pair("", failure.message ?: failure.toString())
}

_failurePopup.value = Event(
_failurePopup.trySend(
PopupUiModel(
title = title,
message = message,
Expand All @@ -75,27 +76,27 @@ abstract class BaseAndroidViewModel(application: Application) : AndroidViewModel
}

protected fun showSnackBar(message: String) {
_success.value = Event(message)
_success.trySend(message)
}

fun navigate(directions: NavDirections) {
_navigation.value = Event(NavigationCommand.ToDirection(directions))
_navigation.trySend(NavigationCommand.ToDirection(directions))
}

fun navigate(deepLink: String) {
_navigation.value = Event(NavigationCommand.ToDeepLink(deepLink))
_navigation.trySend(NavigationCommand.ToDeepLink(deepLink))
}

fun navigate(@StringRes deepLinkRes: Int) {
navigate(getString(deepLinkRes))
}

fun navigate(model: PopupUiModel, callback: PopupCallback?) {
_navigation.value = Event(NavigationCommand.Popup(model, callback))
_navigation.trySend(NavigationCommand.Popup(model, callback))
}

fun navigateBack() {
_navigation.value = Event(NavigationCommand.Back)
_navigation.trySend(NavigationCommand.Back)
}

protected suspend fun <T> runOnViewModelScope(block: suspend CoroutineScope.() -> T): T {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import com.adesso.movee.BR
import com.adesso.movee.internal.extension.observeNonNull
import com.adesso.movee.internal.extension.collectFlow
import com.adesso.movee.internal.extension.showPopup
import com.adesso.movee.internal.util.functional.lazyThreadSafetyNone
import com.adesso.movee.navigation.NavigationCommand
Expand Down Expand Up @@ -88,10 +88,9 @@ abstract class BaseBottomSheetDialogFragment<VM : BaseAndroidViewModel, B : View
}

private fun observeNavigation() {
viewModel.navigation.observeNonNull(viewLifecycleOwner) {
it.getContentIfNotHandled()?.let { command ->
handleNavigation(command)
}

collectFlow(viewModel.navigation) { command ->
handleNavigation(command)
}
}

Expand All @@ -100,33 +99,36 @@ abstract class BaseBottomSheetDialogFragment<VM : BaseAndroidViewModel, B : View
is NavigationCommand.ToDirection -> {
findNavController().navigate(command.directions, getExtras())
}

is NavigationCommand.ToDeepLink -> {
(activity as? MainActivity)
?.navController
?.navigate(command.deepLink.toUri(), null, getExtras())
}

is NavigationCommand.Popup -> {
with(command) {
context?.showPopup(model, callback)
}
}

is NavigationCommand.Back -> findNavController().navigateUp()
}
}

private fun observeFailure() {
viewModel.failurePopup.observeNonNull(viewLifecycleOwner) {
it.getContentIfNotHandled()?.let { popupUiModel ->
context?.showPopup(popupUiModel)
}

collectFlow(viewModel.failurePopup) { popupUiModel ->

context?.showPopup(popupUiModel)
}
}

private fun observeSuccess() {
viewModel.success.observeNonNull(viewLifecycleOwner) {
it.getContentIfNotHandled()?.let { message ->
showSnackBarMessage(message)
}

collectFlow(viewModel.success) { message ->

showSnackBarMessage(message)
}
}

Expand Down
27 changes: 14 additions & 13 deletions app/src/main/kotlin/com/adesso/movee/base/BaseFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import androidx.navigation.fragment.FragmentNavigator
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
import com.adesso.movee.BR
import com.adesso.movee.internal.extension.observeNonNull
import com.adesso.movee.internal.extension.collectFlow
import com.adesso.movee.internal.extension.collectFlowNonNull
import com.adesso.movee.internal.extension.showPopup
import com.adesso.movee.navigation.NavigationCommand
import com.adesso.movee.scene.main.MainActivity
Expand Down Expand Up @@ -70,10 +71,10 @@ abstract class BaseFragment<VM : BaseAndroidViewModel, B : ViewDataBinding> : Fr
}

private fun observeNavigation() {
viewModel.navigation.observeNonNull(viewLifecycleOwner) {
it.getContentIfNotHandled()?.let { command ->
handleNavigation(command)
}

collectFlow(viewModel.navigation) { command ->

handleNavigation(command)
}
}

Expand All @@ -100,18 +101,18 @@ abstract class BaseFragment<VM : BaseAndroidViewModel, B : ViewDataBinding> : Fr
}

private fun observeFailure() {
viewModel.failurePopup.observeNonNull(viewLifecycleOwner) {
it.getContentIfNotHandled()?.let { popupUiModel ->
context?.showPopup(popupUiModel)
}

collectFlow(viewModel.failurePopup) { popupUiModel ->

context?.showPopup(popupUiModel)
}
}

private fun observeSuccess() {
viewModel.success.observeNonNull(viewLifecycleOwner) {
it.getContentIfNotHandled()?.let { message ->
showSnackBarMessage(message)
}

collectFlowNonNull(viewModel.success) { message ->

showSnackBarMessage(message)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package com.adesso.movee.internal.extension
import android.app.Activity
import android.content.Context
import android.view.inputmethod.InputMethodManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.flow.Flow

fun Activity.closeKeyboard() {
currentFocus?.let { view ->
Expand All @@ -17,3 +20,19 @@ fun Activity.showKeyboard() {
imm?.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
}
}

inline fun <T> Activity.collectFlow(
flow: Flow<T>,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
crossinline action: suspend (T) -> Unit
) {
flow.collectWithLifecycle(this as LifecycleOwner, minActiveState, action)
}

inline fun <T> Activity.collectFlowNonNull(
flow: Flow<T?>,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
crossinline action: suspend (T) -> Unit
) {
flow.collectWithLifecycleNonNull(this as LifecycleOwner, minActiveState, action)
}
53 changes: 53 additions & 0 deletions app/src/main/kotlin/com/adesso/movee/internal/extension/Flow.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.adesso.movee.internal.extension

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch

inline fun <T> Flow<T>.collect(
scope: CoroutineScope,
crossinline action: suspend (T) -> Unit
) {
scope.launch {
collect { action(it) }
}
}

inline fun <T> Flow<T?>.collectNonNull(
scope: CoroutineScope,
crossinline action: suspend (T) -> Unit
) {
scope.launch {
collect { value ->
value?.let { action(it) }
}
}
}

inline fun <T> Flow<T>.collectWithLifecycle(
lifecycleOwner: LifecycleOwner,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
crossinline action: suspend (T) -> Unit
) {
lifecycleOwner.lifecycleScope.launch {
flowWithLifecycle(lifecycleOwner.lifecycle, minActiveState)
.collect { action.invoke(it) }
}
}

inline fun <T> Flow<T?>.collectWithLifecycleNonNull(
lifecycleOwner: LifecycleOwner,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
crossinline action: suspend (T) -> Unit
) {
lifecycleOwner.lifecycleScope.launch {
flowWithLifecycle(lifecycleOwner.lifecycle, minActiveState)
.collect { value ->
value?.let { action.invoke(it) }
}
}
}
Loading