Skip to content

Commit

Permalink
Merge branch 'master' of github.com:skydoves/DisneyMotions
Browse files Browse the repository at this point in the history
  • Loading branch information
skydoves committed Jul 17, 2021
2 parents 37d3087 + 83a528d commit 7a0dd70
Show file tree
Hide file tree
Showing 25 changed files with 123 additions and 116 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ Go to the [Releases](https://github.com/skydoves/DisneyMotions/releases) to down
- Minimum SDK level 21
- 100% [Kotlin](https://kotlinlang.org/) based + [Coroutines](https://github.com/Kotlin/kotlinx.coroutines) + [Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/) for asynchronous.
- JetPack
- LiveData - notify domain layer data to views.
- Lifecycle - dispose observing data when lifecycle state changes.
- ViewModel - UI related data holder, lifecycle aware.
- Room Persistence - construct database.
- Architecture
- MVVM Architecture (View - DataBinding - ViewModel - Model)
- [Bindables](https://github.com/skydoves/bindables) - Android DataBinding kit for notifying data changes to UI layers.
- Repository pattern
- [Koin](https://github.com/InsertKoinIO/koin) - dependency injection
- Material Design & Animations
Expand Down
4 changes: 4 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ android {
returnDefaultValues = true
}
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions.freeCompilerArgs += ["-Xopt-in=kotlin.time.ExperimentalTime"]
kotlinOptions.freeCompilerArgs += ["-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"]
}
}

dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import com.skydoves.disneymotions.di.networkModule
import com.skydoves.disneymotions.di.persistenceModule
import com.skydoves.disneymotions.di.repositoryModule
import com.skydoves.disneymotions.di.viewModelModule
import com.skydoves.disneymotions.network.GlobalResponseOperator
import com.skydoves.sandwich.SandwichInitializer
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import timber.log.Timber
Expand All @@ -40,6 +42,9 @@ class DisneyApplication : Application() {
modules(viewModelModule)
}

// initialize global sandwich operator
SandwichInitializer.sandwichOperator = GlobalResponseOperator<Any>(this)

if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.skydoves.disneymotions.binding

import android.widget.Toast
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView
import com.skydoves.baserecyclerviewadapter.BaseAdapter
Expand All @@ -34,14 +33,6 @@ object RecyclerViewBinding {
view.adapter = baseAdapter
}

@JvmStatic
@BindingAdapter("toast")
fun bindToast(view: RecyclerView, text: String?) {
text.whatIfNotNullOrEmpty {
Toast.makeText(view.context, it, Toast.LENGTH_SHORT).show()
}
}

@JvmStatic
@BindingAdapter("adapterPosterList")
fun bindAdapterPosterList(view: RecyclerView, posters: List<Poster>?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ val networkModule = module {
"https://gist.githubusercontent.com/skydoves/aa3bbbf495b0fa91db8a9e89f34e4873/raw/a1a13d37027e8920412da5f00f6a89c5a3dbfb9a/"
)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(CoroutinesResponseCallAdapterFactory())
.addCallAdapterFactory(CoroutinesResponseCallAdapterFactory.create())
.build()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ val viewModelModule = module {

viewModel { MainViewModel(get()) }

viewModel { PosterDetailViewModel(get()) }
viewModel { (posterId: Long) -> PosterDetailViewModel(posterId, get()) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Designed and developed by 2020 skydoves (Jaewoong Eum)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.skydoves.disneymotions.network

import android.app.Application
import android.widget.Toast
import com.skydoves.disneymotions.mapper.ErrorResponseMapper
import com.skydoves.sandwich.ApiResponse
import com.skydoves.sandwich.StatusCode
import com.skydoves.sandwich.map
import com.skydoves.sandwich.message
import com.skydoves.sandwich.operators.ApiResponseSuspendOperator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber

class GlobalResponseOperator<T> constructor(
private val application: Application
) : ApiResponseSuspendOperator<T>() {

override suspend fun onError(apiResponse: ApiResponse.Failure.Error<T>) =
withContext(Dispatchers.Main) {
apiResponse.run {
Timber.d(message())

when (statusCode) {
StatusCode.InternalServerError -> toast("InternalServerError")
StatusCode.BadGateway -> toast("BadGateway")
else -> toast("$statusCode(${statusCode.code}): ${message()}")
}

map(ErrorResponseMapper) {
Timber.d(message())
}
}
}

override suspend fun onException(apiResponse: ApiResponse.Failure.Exception<T>) =
withContext(Dispatchers.Main) {
apiResponse.run {
Timber.d(message())
toast(message())
}
}

override suspend fun onSuccess(apiResponse: ApiResponse.Success<T>) = Unit

private fun toast(message: String) {
Toast.makeText(application, message, Toast.LENGTH_SHORT).show()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ import com.skydoves.sandwich.message
import com.skydoves.sandwich.onError
import com.skydoves.sandwich.onException
import com.skydoves.sandwich.suspendOnSuccess
import com.skydoves.whatif.whatIfNotNull
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onCompletion
import timber.log.Timber

class MainRepository constructor(
Expand All @@ -45,36 +45,31 @@ class MainRepository constructor(
@WorkerThread
fun loadDisneyPosters(
onSuccess: () -> Unit,
onError: (String) -> Unit
) = flow {
val posters: List<Poster> = posterDao.getPosterList()
if (posters.isEmpty()) {
// request API network request asynchronously.
disneyService.fetchDisneyPosterList()
// handles the success case when the API request gets a successful response.
.suspendOnSuccess {
data.whatIfNotNull {
posterDao.insertPosterList(it)
emit(it)
onSuccess()
}
posterDao.insertPosterList(data)
emit(data)
}
/**
* handles error cases when the API request gets an error response.
* e.g., internal server error.
* maps the [ApiResponse.Failure.Error] to the [PosterErrorResponse] using the mapper.
*/
.onError(ErrorResponseMapper) {
onError("[Code: $code]: $message")
Timber.d("[Code: $code]: $message")
}
// handles exceptional cases when the API request gets an exception response.
// e.g., network connection error.
.onException {
onError(message())
Timber.d(message())
}
} else {
emit(posters)
onSuccess()
}
}.flowOn(Dispatchers.IO)
}.onCompletion { onSuccess() }.flowOn(Dispatchers.IO)
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,22 @@ import com.skydoves.disneymotions.databinding.ActivityPosterDetailBinding
import com.skydoves.disneymotions.extensions.applyMaterialTransform
import com.skydoves.disneymotions.model.Poster
import com.skydoves.whatif.whatIfNotNullAs
import org.koin.android.viewmodel.ext.android.getViewModel
import org.koin.android.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf

class PosterDetailActivity :
BindingActivity<ActivityPosterDetailBinding>(R.layout.activity_poster_detail) {

private val posterId: Long by bundle(EXTRA_POSTER_ID, -1)
private val posterName: String by bundleNonNull(EXTRA_POSTER_NAME)
private val viewModel: PosterDetailViewModel by viewModel { parametersOf(posterId) }

override fun onCreate(savedInstanceState: Bundle?) {
applyMaterialTransform(posterName)
super.onCreate(savedInstanceState)
binding {
vm = getViewModel<PosterDetailViewModel>().getPoster(posterId)
lifecycleOwner = this@PosterDetailActivity
activity = this@PosterDetailActivity
container = detailContainer
vm = viewModel
fab = fabMore
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,25 @@

package com.skydoves.disneymotions.view.ui.details

import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.switchMap
import com.skydoves.disneymotions.base.LiveCoroutinesViewModel
import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope
import com.skydoves.bindables.BindingViewModel
import com.skydoves.bindables.asBindingProperty
import com.skydoves.disneymotions.model.Poster
import com.skydoves.disneymotions.repository.DetailRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapLatest

class PosterDetailViewModel(
posterId: Long,
private val repository: DetailRepository
) : LiveCoroutinesViewModel() {
) : BindingViewModel() {

private val posterIdLiveData: MutableLiveData<Long> = MutableLiveData()
val poster: LiveData<Poster> = posterIdLiveData.switchMap {
launchOnViewModelScope {
repository.getPosterById(it).asLiveData()
}
private val posterIdStateFlow: MutableStateFlow<Long> = MutableStateFlow(posterId)
private val posterFlow = posterIdStateFlow.flatMapLatest { id ->
repository.getPosterById(id)
}

@MainThread
fun getPoster(id: Long) = apply {
posterIdLiveData.value = id
}
@get:Bindable
val poster: Poster? by posterFlow.asBindingProperty(viewModelScope, null)
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ class HomeFragment : BindingFragment<FragmentHomeBinding>(R.layout.fragment_home
super.onCreateView(inflater, container, savedInstanceState)
return binding {
viewModel = getSharedViewModel()
lifecycleOwner = viewLifecycleOwner
adapter = PosterAdapter()
}.root
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ class LibraryFragment : BindingFragment<FragmentLibraryBinding>(R.layout.fragmen
super.onCreateView(inflater, container, savedInstanceState)
return binding {
viewModel = getSharedViewModel()
lifecycleOwner = viewLifecycleOwner
adapter = PosterLineAdapter()
}.root
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ class MainActivity : BindingActivity<ActivityMainBinding>(R.layout.activity_main
super.onCreate(savedInstanceState)
binding {
pagerAdapter = MainPagerAdapter(this@MainActivity)
lifecycleOwner = this@MainActivity
vm = getViewModel()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,30 @@
package com.skydoves.disneymotions.view.ui.main

import androidx.databinding.Bindable
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.skydoves.bindables.BindingViewModel
import com.skydoves.bindables.asBindingProperty
import com.skydoves.bindables.bindingProperty
import com.skydoves.disneymotions.base.LiveCoroutinesViewModel
import com.skydoves.disneymotions.model.Poster
import com.skydoves.disneymotions.repository.MainRepository
import timber.log.Timber

class MainViewModel constructor(
private val mainRepository: MainRepository
) : LiveCoroutinesViewModel() {

val posterListLiveData: LiveData<List<Poster>>
mainRepository: MainRepository
) : BindingViewModel() {

@get:Bindable
var isLoading: Boolean by bindingProperty(true)
private set

private val posterListFlow = mainRepository.loadDisneyPosters(
onSuccess = { isLoading = false }
)

@get:Bindable
var errorToast: String? by bindingProperty(null)
private set
val posterList: List<Poster> by posterListFlow.asBindingProperty(viewModelScope, emptyList())

init {
Timber.d("injection MainViewModel")

posterListLiveData = launchOnViewModelScope {
this.mainRepository.loadDisneyPosters(
onSuccess = { isLoading = false },
onError = { errorToast = it }
).asLiveData()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ class RadioFragment : BindingFragment<FragmentRadioBinding>(R.layout.fragment_ra
super.onCreateView(inflater, container, savedInstanceState)
return binding {
viewModel = getSharedViewModel()
lifecycleOwner = viewLifecycleOwner
adapter = PosterCircleAdapter()
}.root
}
Expand Down
Loading

0 comments on commit 7a0dd70

Please sign in to comment.