Skip to content

Commit ffeca9a

Browse files
Merge branch 'master' into troubleshooting/paging_adapter_scrolling_bug
# Conflicts: # app/src/main/java/com/intive/tmdbandroid/home/ui/HomeFragment.kt # app/src/main/res/layout/item_screening.xml
2 parents 6289469 + c624ca2 commit ffeca9a

35 files changed

+1102
-81
lines changed

.github/workflows/android.yml

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,84 @@ name: TMDB CI
22

33
on:
44
push:
5-
branches: [ master ]
5+
branches: [ master, feature/github_actions ]
66
pull_request:
7-
branches: [ master ]
7+
branches: [ master, feature/github_actions ]
88

99
jobs:
1010
test:
1111
name: Run Unit Tests
12-
runs-on: ubuntu-18.04
12+
runs-on: ubuntu-latest #Each job runs in a runner environment specified
1313
steps:
14-
- uses: actions/checkout@v1
15-
- name: set up JDK 1.8
16-
uses: actions/setup-java@v1
14+
- uses: actions/checkout@v2.3.3
15+
- name: set up JDK 11
16+
uses: actions/setup-java@v2
1717
with:
18-
java-version: 1.8
18+
java-version: '11'
19+
distribution: 'adopt'
20+
cache: gradle
1921
- name: Unit tests
2022
run: bash ./gradlew test --stacktrace
2123
- name: Unit tests results
22-
uses: actions/upload-artifact@v1
24+
uses: actions/upload-artifact@v2.2.3
2325
with:
2426
name: unit-tests-results
2527
path: app/build/reports/tests/testDebugUnitTest/index.html
2628

29+
lint:
30+
name: Lint Check
31+
runs-on: ubuntu-latest
32+
steps:
33+
- uses: actions/checkout@v2.3.3
34+
- name: set up JDK 11
35+
uses: actions/setup-java@v2
36+
with:
37+
java-version: '11'
38+
distribution: 'adopt'
39+
cache: gradle
40+
- name: Lint debug flavor
41+
run: bash ./gradlew lintDebug --stacktrace
42+
- name: Lint results
43+
uses: actions/upload-artifact@v2.2.3
44+
with:
45+
name: lint-result
46+
path: app/build/reports/lint-results-debug.html
47+
2748
build:
2849
runs-on: ubuntu-latest
2950
steps:
30-
- uses: actions/checkout@v2
51+
- uses: actions/checkout@v2.3.3
3152
- name: set up JDK 11
3253
uses: actions/setup-java@v2
3354
with:
3455
java-version: '11'
3556
distribution: 'adopt'
3657
cache: gradle
3758

38-
- name: Grant execute permission for gradlew
39-
run: chmod +x gradlew
4059
- name: Build with Gradle
41-
run: ./gradlew build
60+
run: bash ./gradlew assembleDebug --stacktrace
61+
62+
- name: Upload APK
63+
uses: actions/upload-artifact@v2.2.3
64+
with:
65+
name: app
66+
path: app/build/outputs/apk/debug/*.apk
67+
68+
- name: Send mail
69+
if: always()
70+
uses: dawidd6/action-send-mail@v2
71+
with:
72+
# mail server settings
73+
server_address: smtp.gmail.com
74+
server_port: 465
75+
# user credentials
76+
username: ${{ secrets.EMAIL_USERNAME }}
77+
password: ${{ secrets.EMAIL_PASSWORD }}
78+
# email subject
79+
subject: ${{ github.job }} job of ${{ github.repository }} has ${{ job.status }}
80+
# email body as text
81+
body: ${{ github.job }} job in worflow ${{ github.workflow }} of ${{ github.repository }} has ${{ job.status }}
82+
# comma-separated string, send email to
83+
to: francisco.beccuti@intive.com
84+
# from email name
85+
from: TMDB Team! GitHub Actions!

app/build.gradle

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,18 @@ plugins {
55
id 'dagger.hilt.android.plugin'
66
id 'androidx.navigation.safeargs.kotlin'
77
id 'kotlin-parcelize'
8+
id "org.sonarqube" version "3.3"
89
}
910

1011
android {
1112
compileSdk 30
13+
compileOptions {
14+
// Flag to enable support for the new language APIs
15+
coreLibraryDesugaringEnabled true
16+
// Sets Java compatibility to Java 8
17+
sourceCompatibility JavaVersion.VERSION_1_8
18+
targetCompatibility JavaVersion.VERSION_1_8
19+
}
1220

1321
def _major
1422
def _minor
@@ -96,6 +104,7 @@ dependencies {
96104
implementation "com.github.bumptech.glide:glide:$glideVersion"
97105
implementation "androidx.palette:palette-ktx:$paletteVersion"
98106
implementation "com.jakewharton.timber:timber:$timberVersion"
107+
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
99108
implementation "androidx.room:room-ktx:$roomVersion"
100109

101110
testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.intive.tmdbandroid.datasource
2+
3+
import androidx.paging.PagingSource
4+
import com.intive.tmdbandroid.entity.ResultTVShowsEntity
5+
import androidx.paging.PagingState
6+
import com.intive.tmdbandroid.datasource.network.Service
7+
import com.intive.tmdbandroid.model.TVShow
8+
import kotlinx.coroutines.flow.collect
9+
10+
class TVShowSearchSource(private val service: Service, private val query: String) : PagingSource<Int, TVShow>() {
11+
companion object {
12+
const val DEFAULT_PAGE_INDEX = 1
13+
}
14+
15+
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TVShow> {
16+
return try {
17+
val pageNumber = params.key ?: DEFAULT_PAGE_INDEX
18+
lateinit var response: ResultTVShowsEntity
19+
service.getTvShowByTitle(query, pageNumber).collect { response = it }
20+
21+
val prevKey = if (pageNumber > DEFAULT_PAGE_INDEX) pageNumber - 1 else null
22+
val nextKey = if (response.TVShows.isNotEmpty()) pageNumber + 1 else null
23+
24+
LoadResult.Page(
25+
data = response.toTVShowList(),
26+
prevKey = prevKey,
27+
nextKey = nextKey
28+
)
29+
} catch (e: Exception) {
30+
LoadResult.Error(e)
31+
}
32+
}
33+
34+
override fun getRefreshKey(state: PagingState<Int, TVShow>): Int? {
35+
return state.anchorPosition?.let {
36+
state.closestPageToPosition(it)?.prevKey?.plus(1)
37+
?: state.closestPageToPosition(it)?.nextKey?.minus(1)
38+
}
39+
}
40+
}

app/src/main/java/com/intive/tmdbandroid/datasource/local/LocalStorage.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import com.intive.tmdbandroid.model.converter.CreatedByConverter
88
import com.intive.tmdbandroid.model.converter.GenreConverter
99

1010
@Database(entities = [(TVShowORMEntity::class)], version = 1, exportSchema = false)
11-
@TypeConverters(CreatedByConverter::class,GenreConverter::class)
11+
@TypeConverters(CreatedByConverter::class, GenreConverter::class)
1212
abstract class LocalStorage : RoomDatabase() {
1313
abstract fun tvShowDao(): Dao
1414
}

app/src/main/java/com/intive/tmdbandroid/datasource/network/ApiClient.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,9 @@ interface ApiClient {
1414
@GET("tv/{tv_id}")
1515
suspend fun getTVShowByID(@Path("tv_id") tvShowID: Int,
1616
@Query("api_key") apiKey: String) : TVShow
17+
18+
@GET("search/tv")
19+
suspend fun getTVShowByName(@Query("api_key") apiKey: String,
20+
@Query("query") query: String,
21+
@Query("page") page: Int) : ResultTVShowsEntity
1722
}

app/src/main/java/com/intive/tmdbandroid/datasource/network/Service.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,21 @@ import kotlinx.coroutines.flow.flow
1010
class Service {
1111
private val retrofit = RetrofitHelper.getRetrofit()
1212

13-
fun getPaginatedPopularTVShows(page: Int) : Flow<ResultTVShowsEntity> {
13+
fun getPaginatedPopularTVShows(page: Int): Flow<ResultTVShowsEntity> {
1414
return flow {
1515
emit(retrofit.create(ApiClient::class.java).getPaginatedPopularTVShows(BuildConfig.API_KEY, page))
1616
}
1717
}
1818

19-
fun getTVShowByID(tvShowID: Int) : Flow<TVShow> {
19+
fun getTVShowByID(tvShowID: Int): Flow<TVShow> {
2020
return flow {
2121
emit(retrofit.create(ApiClient::class.java).getTVShowByID(tvShowID, BuildConfig.API_KEY))
2222
}
2323
}
24-
}
24+
25+
fun getTvShowByTitle(tvShowTitle: String, page: Int): Flow<ResultTVShowsEntity> {
26+
return flow {
27+
emit(retrofit.create(ApiClient::class.java).getTVShowByName(BuildConfig.API_KEY, tvShowTitle, page))
28+
}
29+
}
30+
}

app/src/main/java/com/intive/tmdbandroid/details/ui/DetailFragment.kt

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import com.intive.tmdbandroid.details.viewmodel.DetailsViewModel
2323
import com.intive.tmdbandroid.model.TVShow
2424
import dagger.hilt.android.AndroidEntryPoint
2525
import kotlinx.coroutines.flow.collect
26+
import kotlinx.coroutines.flow.collectLatest
27+
import kotlinx.coroutines.launch
2628
import java.text.SimpleDateFormat
2729
import java.util.*
2830

@@ -47,8 +49,8 @@ class DetailFragment : Fragment() {
4749
): View {
4850
val binding = FragmentDetailBinding.inflate(inflater, container, false)
4951

50-
collectDataFromViewModel(binding)
51-
setupToolbar(binding)
52+
collectTVShowDetailFromViewModel(binding)
53+
collectWatchlistDataFromViewModel(binding)
5254

5355
return binding.root
5456
}
@@ -65,7 +67,7 @@ class DetailFragment : Fragment() {
6567
Glide.get(requireContext()).clearMemory()
6668
}
6769

68-
private fun collectDataFromViewModel(binding: FragmentDetailBinding) {
70+
private fun collectTVShowDetailFromViewModel(binding: FragmentDetailBinding) {
6971
binding.coordinatorContainerDetail.visibility = View.INVISIBLE
7072
lifecycleScope.launchWhenCreated {
7173
viewModel.uiState.collect { state ->
@@ -89,6 +91,30 @@ class DetailFragment : Fragment() {
8991
}
9092
}
9193

94+
private fun collectWatchlistDataFromViewModel(binding: FragmentDetailBinding) {
95+
lifecycleScope.launch {
96+
viewModel.watchlistUIState.collectLatest {
97+
when (it) {
98+
is State.Success -> {
99+
binding.layoutErrorDetail.errorContainer.visibility = View.GONE
100+
binding.layoutLoadingDetail.progressBar.visibility = View.GONE
101+
selectOrUnselectWatchlistFav(binding, it.data)
102+
isSaveOnWatchlist = it.data
103+
}
104+
State.Error -> {
105+
binding.layoutLoadingDetail.progressBar.visibility = View.GONE
106+
binding.layoutErrorDetail.errorContainer.visibility = View.VISIBLE
107+
binding.coordinatorContainerDetail.visibility = View.VISIBLE
108+
}
109+
State.Loading -> {
110+
binding.layoutErrorDetail.errorContainer.visibility = View.GONE
111+
binding.layoutLoadingDetail.progressBar.visibility = View.VISIBLE
112+
}
113+
}
114+
}
115+
}
116+
}
117+
92118
private fun setupUI(binding: FragmentDetailBinding, tvShow: TVShow) {
93119

94120
setImages(binding, tvShow)
@@ -97,6 +123,8 @@ class DetailFragment : Fragment() {
97123

98124
setPercentageToCircularPercentage(binding, tvShow.vote_average)
99125

126+
setupToolbar(binding, tvShow)
127+
100128
binding.toolbar.title = tvShow.name
101129

102130
binding.statusDetailTextView.text = tvShow.status
@@ -126,6 +154,8 @@ class DetailFragment : Fragment() {
126154

127155
binding.overviewDetailTextView.text = tvShow.overview
128156
binding.coordinatorContainerDetail.visibility = View.VISIBLE
157+
158+
tvShowId?.let { viewModel.existAsFavorite(it) }
129159
}
130160

131161
private fun setPercentageToCircularPercentage(
@@ -139,18 +169,23 @@ class DetailFragment : Fragment() {
139169
val context = binding.root.context
140170

141171
when {
142-
percentage < 25 -> binding.circularPercentage.progressTintList = ContextCompat.getColorStateList(context, R.color.red)
143-
percentage < 45 -> binding.circularPercentage.progressTintList = ContextCompat.getColorStateList(context, R.color.orange)
144-
percentage < 75 -> binding.circularPercentage.progressTintList = ContextCompat.getColorStateList(context, R.color.yellow)
145-
else -> binding.circularPercentage.progressTintList = ContextCompat.getColorStateList(context, R.color.green)
172+
percentage < 25 -> binding.circularPercentage.progressTintList =
173+
ContextCompat.getColorStateList(context, R.color.red)
174+
percentage < 45 -> binding.circularPercentage.progressTintList =
175+
ContextCompat.getColorStateList(context, R.color.orange)
176+
percentage < 75 -> binding.circularPercentage.progressTintList =
177+
ContextCompat.getColorStateList(context, R.color.yellow)
178+
else -> binding.circularPercentage.progressTintList =
179+
ContextCompat.getColorStateList(context, R.color.green)
146180
}
147181
binding.screeningPopularity.text = resources.getString(R.string.popularity, percentage)
148182
}
149183

150184
private fun setDate(binding: FragmentDetailBinding, firstAirDate: String) {
151185
try {
152186
val date = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(firstAirDate)
153-
val stringDate = date?.let { SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()).format(it) }
187+
val stringDate =
188+
date?.let { SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()).format(it) }
154189
binding.firstAirDateDetailTextView.text = stringDate
155190
} catch (e: Exception) {
156191
binding.firstAirDateDetailTextView.text = ""
@@ -176,15 +211,19 @@ class DetailFragment : Fragment() {
176211
.into(binding.backgroundImageToolbarLayout)
177212
}
178213

179-
private fun setupToolbar(binding: FragmentDetailBinding) {
214+
private fun setupToolbar(binding: FragmentDetailBinding, tvShow: TVShow) {
180215
val navController = findNavController()
181216
val appBarConfiguration = AppBarConfiguration(navController.graph)
182217
val toolbar = binding.toolbar
183218
toolbar.inflateMenu(R.menu.watchlist_favorite_detail_fragment)
184219
toolbar.setOnMenuItemClickListener {
185-
when(it.itemId) {
220+
when (it.itemId) {
186221
R.id.ic_heart_watchlist -> {
187-
selectOrUnselectWatchlistFav(binding)
222+
if (!isSaveOnWatchlist) {
223+
viewModel.addToWatchlist(tvShow.toTVShowORMEntity())
224+
} else {
225+
viewModel.deleteFromWatchlist(tvShow.toTVShowORMEntity())
226+
}
188227
true
189228
}
190229
else -> false
@@ -202,13 +241,14 @@ class DetailFragment : Fragment() {
202241
})
203242
}
204243

205-
private fun selectOrUnselectWatchlistFav(binding: FragmentDetailBinding) {
206-
isSaveOnWatchlist = !isSaveOnWatchlist
244+
private fun selectOrUnselectWatchlistFav(binding: FragmentDetailBinding, isFav: Boolean) {
207245
val watchlistItem = binding.toolbar.menu.findItem(R.id.ic_heart_watchlist)
208-
if (isSaveOnWatchlist){
209-
watchlistItem.icon = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_heart_selected)
210-
}else {
211-
watchlistItem.icon = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_heart_unselected)
212-
}
246+
if (isFav) {
247+
watchlistItem.icon =
248+
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_heart_selected)
249+
} else watchlistItem.icon =
250+
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_heart_unselected)
251+
213252
}
253+
214254
}

0 commit comments

Comments
 (0)