Skip to content

Commit

Permalink
replace livedata with coroutine flows and channels
Browse files Browse the repository at this point in the history
  • Loading branch information
alicankorkmaz-sudo committed Feb 22, 2023
1 parent e00a510 commit d6c8534
Show file tree
Hide file tree
Showing 25 changed files with 293 additions and 310 deletions.
68 changes: 44 additions & 24 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,17 +72,21 @@ 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
- **[Lottie](https://github.com/airbnb/lottie-android):** JSON based animations
- **[Retrofit](https://github.com/square/retrofit):** Type safe HTTP client
- **[Moshi](https://github.com/square/moshi):** JSON serializer/deserializer
- **[Room](https://developer.android.com/topic/libraries/architecture/room):** Object mapping for SQLite
- **[Room](https://developer.android.com/topic/libraries/architecture/room):** Object mapping for
SQLite

#### Plugins

Expand Down Expand Up @@ -99,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
34 changes: 19 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,33 +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.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

@Suppress("ConvertSecondaryConstructorToPrimary")
@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()

private val viewModelJob = SupervisorJob()

Expand All @@ -43,31 +42,36 @@ abstract class BaseAndroidViewModel(application: Application) : AndroidViewModel
"",
getString(R.string.common_error_network_connection)
)

is Failure.UnknownHostError -> Pair("", getString(R.string.common_error_unknown_host))
is Failure.ServerError -> Pair("", failure.message)
is Failure.JsonError, is Failure.EmptyResponse -> Pair(
"",
getString(R.string.common_error_invalid_response)
)

is Failure.FormValidationError -> Pair(
getString(R.string.common_title_popup_form_validation),
failure.message
?: getString(R.string.common_error_invalid_form)
)

is Failure.IoError -> Pair("", getString(R.string.common_error_can_not_save_data))
is Failure.UnknownError -> Pair(
"",
failure.exception.localizedMessage ?: getString(R.string.common_error_unknown)
)

is Failure.HttpError -> Pair(
"",
getString(R.string.common_error_http, failure.code.toString())
)

is Failure.TimeOutError -> Pair("", getString(R.string.common_error_timeout))
else -> Pair("", failure.message ?: failure.toString())
}

_failurePopup.value = Event(
_failurePopup.trySend(
PopupUiModel(
title = title,
message = message,
Expand All @@ -77,27 +81,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 onUIThread(block: suspend CoroutineScope.() -> Unit) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,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 @@ -103,10 +103,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 @@ -115,33 +114,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
30 changes: 17 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 @@ -18,7 +18,8 @@ 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.collectFlowNonNull
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 +89,10 @@ abstract class BaseFragment<VM : BaseAndroidViewModel, B : ViewDataBinding> :
}

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

collectFlow(viewModel.navigation) { command ->

handleNavigation(command)
}
}

Expand All @@ -100,33 +101,36 @@ abstract class BaseFragment<VM : BaseAndroidViewModel, B : ViewDataBinding> :
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)
}

collectFlowNonNull(viewModel.success) { message ->

showSnackBarMessage(message)
}
}

Expand Down
Loading

0 comments on commit d6c8534

Please sign in to comment.