Merge pull request #10 from ashwini009/di-improvement

Faster initialisation via enhanced DI, Improved test, More Kotlin sweetness
This commit is contained in:
Ashwini Kumar
2020-06-15 21:35:02 +05:30
committed by GitHub
30 changed files with 352 additions and 240 deletions

View File

@ -1,17 +1,17 @@
name: Android CI
on: [push]
on:
push:
branches:
- master
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build with Gradle
run: ./gradlew build
- uses: actions/checkout@v1
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build with Gradle
run: ./gradlew assembleDebug

View File

@ -1,5 +1,4 @@
apply plugin: 'com.android.application'
apply from: "$project.rootDir/tools/findbugs.gradle"
apply from: "$project.rootDir/tools/checkstyle.gradle"
apply from: "$project.rootDir/tools/pmd.gradle"
apply plugin: 'kotlin-android'
@ -61,6 +60,10 @@ android {
androidExtensions {
experimental = true
}
kotlinOptions {
jvmTarget = "1.8"
}
}
apply from: 'dependencies.gradle'

View File

@ -1,20 +1,19 @@
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//Support
implementation deps.support_lib.design
implementation deps.support_lib.appcompat
implementation deps.support_lib.support_annotations
implementation deps.support_lib.recyclerview
implementation deps.support_lib.cardview
implementation deps.androidx_lib.design
implementation deps.androidx_lib.appcompat
implementation deps.androidx_lib.support_annotations
implementation deps.androidx_lib.recyclerview
implementation deps.androidx_lib.cardview
implementation deps.androidx_lib.ktx_core
implementation deps.androidx_lib.ktx_collections
implementation deps.androidx_lib.ktx_fragment
implementation deps.androidx_lib.ktx_activity
implementation deps.constraint_layout
implementation deps.multidex
implementation deps.rxjava.android
// Because RxAndroid releases are few and far between, it is recommended you also
// explicitly depend on RxJava's latest version for bug fixes and new features.
implementation deps.rxjava.main
implementation deps.okhttp.main
implementation deps.okhttp.logging_interceptor
@ -39,6 +38,7 @@ dependencies {
implementation deps.lifecycle.extensions
kapt deps.lifecycle.compiler
implementation deps.lifecycle.viewmodel_ktx
implementation deps.lifecycle.livedata_ktx
// Paging
implementation deps.paging.runtime
@ -47,7 +47,6 @@ dependencies {
// Room
implementation deps.room.runtime
kapt deps.room.compiler
implementation deps.room.rxjava2
testImplementation deps.room.testing
implementation deps.room.ktx
@ -82,4 +81,7 @@ dependencies {
implementation deps.coroutines.rx2
implementation deps.coroutines.android
testImplementation deps.coroutines.test
debugImplementation deps.chucker.debug
releaseImplementation deps.chucker.release
}

View File

@ -1,20 +1,26 @@
package com.android.tvmaze.di
import com.android.tvmaze.favorite.FavoriteShowsActivity
import com.android.tvmaze.favorite.FavoriteShowsModule
import com.android.tvmaze.home.HomeActivity
import com.android.tvmaze.home.HomeModule
import com.android.tvmaze.shows.AllShowsActivity
import com.android.tvmaze.shows.ShowsModule
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class ActivityBuildersModule {
@ContributesAndroidInjector
@ActivityScoped
@ContributesAndroidInjector(modules = [HomeModule::class])
abstract fun bindHomeActivity(): HomeActivity
@ContributesAndroidInjector
@ActivityScoped
@ContributesAndroidInjector(modules = [ShowsModule::class])
abstract fun bindAllShowsActivity(): AllShowsActivity
@ContributesAndroidInjector
@ActivityScoped
@ContributesAndroidInjector(modules = [FavoriteShowsModule::class])
abstract fun bindFavoriteShowsActivity(): FavoriteShowsActivity
}

View File

@ -10,7 +10,13 @@ import dagger.android.support.AndroidSupportInjectionModule
import javax.inject.Singleton
@Singleton
@Component(modules = [AndroidSupportInjectionModule::class, AppModule::class, ActivityBuildersModule::class, NetworkModule::class, TvMazeDbModule::class])
@Component(
modules = [AndroidSupportInjectionModule::class,
AppModule::class,
ActivityBuildersModule::class,
NetworkModule::class,
TvMazeDbModule::class]
)
interface AppComponent {
fun inject(tvMazeApplication: TvMazeApplication)

View File

@ -6,9 +6,8 @@ import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module(includes = [ViewModelModule::class])
@Module
object AppModule {
@JvmStatic
@Provides
@Singleton
fun provideContext(application: Application): Context {

View File

@ -0,0 +1,8 @@
package com.android.tvmaze.di
import javax.inject.Scope
@Scope
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScoped

View File

@ -0,0 +1,7 @@
package com.android.tvmaze.di
/**
* Sugar over multibindings that helps with Kotlin wildcards.
*/
typealias DaggerSet<T> = @JvmSuppressWildcards Set<T>
typealias DaggerMap<K, V> = @JvmSuppressWildcards Map<K, V>

View File

@ -7,9 +7,9 @@ import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton
@Singleton
@ActivityScoped
class TvMazeViewModelFactory @Inject
constructor(private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>) :
constructor(private val creators: DaggerMap<Class<out ViewModel>, Provider<ViewModel>>) :
ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")

View File

@ -0,0 +1,11 @@
package com.android.tvmaze.di
import androidx.lifecycle.ViewModelProvider
import dagger.Binds
import dagger.Module
@Module
abstract class ViewModelFactoryBindingModule {
@Binds
abstract fun bindViewModelFactory(tvMazeViewModelFactory: TvMazeViewModelFactory): ViewModelProvider.Factory
}

View File

@ -1,31 +0,0 @@
package com.android.tvmaze.di
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.android.tvmaze.favorite.FavoriteShowsViewModel
import com.android.tvmaze.home.HomeViewModel
import com.android.tvmaze.shows.ShowsViewModel
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
@Module
abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(HomeViewModel::class)
abstract fun bindHomeViewModel(homeViewModel: HomeViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(ShowsViewModel::class)
abstract fun bindShowsViewModel(showsViewModel: ShowsViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(FavoriteShowsViewModel::class)
abstract fun bindFavoriteShowsViewModel(favoriteShowsViewModel: FavoriteShowsViewModel): ViewModel
@Binds
abstract fun bindViewModelFactory(factory: TvMazeViewModelFactory): ViewModelProvider.Factory
}

View File

@ -8,31 +8,30 @@ import android.text.Spanned
import android.text.style.ImageSpan
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.GridLayoutManager
import com.android.tvmaze.R
import com.android.tvmaze.base.TvMazeBaseActivity
import com.android.tvmaze.db.favouriteshow.FavoriteShow
import com.android.tvmaze.di.ActivityScoped
import com.android.tvmaze.utils.GridItemDecoration
import kotlinx.android.synthetic.main.activity_favorite_shows.*
import kotlinx.android.synthetic.main.toolbar.view.*
import javax.inject.Inject
class FavoriteShowsActivity : TvMazeBaseActivity(), FavoriteShowsAdapter.Callback {
@ActivityScoped
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var favoriteShowsViewModel: FavoriteShowsViewModel
private val favoriteShowsViewModel by viewModels<FavoriteShowsViewModel> { viewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_favorite_shows)
setToolbar()
favoriteShowsViewModel = ViewModelProviders.of(this, viewModelFactory)
.get(FavoriteShowsViewModel::class.java)
favoriteShowsViewModel.loadFavoriteShows()
favoriteShowsViewModel.getFavoriteShowsLiveData()
.observe(this, Observer { showFavorites(it) })
@ -55,7 +54,7 @@ class FavoriteShowsActivity : TvMazeBaseActivity(), FavoriteShowsAdapter.Callbac
val favoriteShowsAdapter = FavoriteShowsAdapter(favoriteShows.toMutableList(), this)
shows.adapter = favoriteShowsAdapter
val spacing = resources.getDimensionPixelSize(R.dimen.show_grid_spacing)
shows.addItemDecoration(GridItemDecoration(spacing, COLUMNS_COUNT))
shows.addItemDecoration(GridItemDecoration(spacing, COLUMNS_COUNT))
shows.visibility = View.VISIBLE
} else {
val bookmarkSpan = ImageSpan(this, R.drawable.favorite_border)

View File

@ -0,0 +1,16 @@
package com.android.tvmaze.favorite
import androidx.lifecycle.ViewModel
import com.android.tvmaze.di.ViewModelFactoryBindingModule
import com.android.tvmaze.di.ViewModelKey
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
@Module(includes = [ViewModelFactoryBindingModule::class])
abstract class FavoriteShowsModule {
@Binds
@IntoMap
@ViewModelKey(FavoriteShowsViewModel::class)
abstract fun bindFavoriteShowsViewModel(favoriteShowsViewModel: FavoriteShowsViewModel): ViewModel
}

View File

@ -5,13 +5,14 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.GridLayoutManager
import com.android.tvmaze.R
import com.android.tvmaze.base.TvMazeBaseActivity
import com.android.tvmaze.di.ActivityScoped
import com.android.tvmaze.favorite.FavoriteShowsActivity
import com.android.tvmaze.shows.AllShowsActivity
import com.android.tvmaze.utils.GridItemDecoration
@ -20,15 +21,15 @@ import kotlinx.android.synthetic.main.toolbar.view.*
import javax.inject.Inject
class HomeActivity : TvMazeBaseActivity(), ShowsAdapter.Callback {
@ActivityScoped
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var homeViewModel: HomeViewModel
private val homeViewModel by viewModels<HomeViewModel> { viewModelFactory }
private lateinit var showsAdapter: ShowsAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
homeViewModel = ViewModelProviders.of(this, viewModelFactory).get(HomeViewModel::class.java)
setToolbar()
homeViewModel.onScreenCreated()
homeViewModel.getHomeViewState().observe(this, Observer { setViewState(it) })

View File

@ -0,0 +1,16 @@
package com.android.tvmaze.home
import androidx.lifecycle.ViewModel
import com.android.tvmaze.di.ViewModelFactoryBindingModule
import com.android.tvmaze.di.ViewModelKey
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
@Module(includes = [ViewModelFactoryBindingModule::class])
abstract class HomeModule {
@Binds
@IntoMap
@ViewModelKey(HomeViewModel::class)
abstract fun bindHomeViewModel(homeViewModel: HomeViewModel): ViewModel
}

View File

@ -3,13 +3,16 @@ package com.android.tvmaze.home
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
import com.android.tvmaze.favorite.FavoriteShowsRepository
import com.android.tvmaze.network.TvMazeApi
import com.android.tvmaze.network.home.Episode
import com.android.tvmaze.network.home.Show
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Calendar
@ -30,15 +33,17 @@ class HomeViewModel @Inject constructor(
val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
onError(exception)
}
// viewModelScope launch the new coroutine on Main Dispatcher internally
// viewModelScope launch the new coroutine on Main Dispatcher internally, so move to db and network opn on IO
viewModelScope.launch(coroutineExceptionHandler) {
// Get favorite shows from db, suspend function in room will launch a new coroutine with IO dispatcher
val favoriteShowIds = favoriteShowsRepository.allFavoriteShowIds()
// Get shows from network, suspend function in retrofit will launch a new coroutine with IO dispatcher
val episodes = tvMazeApi.getCurrentSchedule(COUNTRY_US, currentDate)
// Return the result on main thread via Dispatchers.Main
homeViewStateLiveData.value = Success(HomeViewData(getShowsWithFavorites(episodes, favoriteShowIds)))
withContext(Dispatchers.IO) {
val favoriteShowIds = favoriteShowsRepository.allFavoriteShowIds()
val episodes = tvMazeApi.getCurrentSchedule(COUNTRY_US, currentDate)
withContext(Dispatchers.Main) {
// Return the result on main thread via Dispatchers.Main
homeViewStateLiveData.value =
Success(HomeViewData(getShowsWithFavorites(episodes, favoriteShowIds)))
}
}
}
}

View File

@ -3,8 +3,12 @@ package com.android.tvmaze.network
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ApiInterceptor @Inject constructor() : Interceptor {
class ApiInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()

View File

@ -0,0 +1,23 @@
package com.android.tvmaze.network
import com.chuckerteam.chucker.api.ChuckerInterceptor
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet
import okhttp3.Interceptor
import okhttp3.logging.HttpLoggingInterceptor
@Module
abstract class InterceptorModule {
@Binds
@IntoSet
abstract fun bindApiInterceptor(apiInterceptor: ApiInterceptor): Interceptor
@Binds
@IntoSet
abstract fun bindHttpLoggingInterceptor(httpLoggingInterceptor: HttpLoggingInterceptor): Interceptor
@Binds
@IntoSet
abstract fun bindChuckerInterceptor(chuckerInterceptor: ChuckerInterceptor): Interceptor
}

View File

@ -1,28 +1,38 @@
package com.android.tvmaze.network
import android.content.Context
import android.os.Looper
import com.android.tvmaze.BuildConfig
import com.android.tvmaze.di.DaggerSet
import com.chuckerteam.chucker.api.ChuckerInterceptor
import dagger.Lazy
import dagger.Module
import dagger.Provides
import okhttp3.Cache
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Named
import javax.inject.Qualifier
import javax.inject.Singleton
@Module(includes = [TvMazeApiModule::class])
@Retention(AnnotationRetention.BINARY)
@Qualifier
private annotation class InternalApi
@Module(includes = [TvMazeApiModule::class, InterceptorModule::class])
object NetworkModule {
const val TVMAZE_BASE_URL = "tvmaze_base_url"
private const val BASE_URL = "https://api.tvmaze.com/"
@JvmStatic
@Provides
@Named(TVMAZE_BASE_URL)
fun provideBaseUrlString(): String {
return BASE_URL
}
@JvmStatic
@Provides
@Singleton
fun provideLoggingInterceptor(): HttpLoggingInterceptor {
@ -33,22 +43,55 @@ object NetworkModule {
}
}
@JvmStatic
@Provides
@Singleton
fun provideOkHttpClient(loggingInterceptor: HttpLoggingInterceptor, cache: Cache): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.cache(cache)
.build()
fun provideChuckInterceptor(context: Context): ChuckerInterceptor {
return ChuckerInterceptor(context)
}
@JvmStatic
// Use newBuilder() to customize so that thread-pool and connection-pool same are used
@Provides
fun provideOkHttpClientBuilder(
@InternalApi okHttpClient: Lazy<OkHttpClient>
): OkHttpClient.Builder {
return okHttpClient.get().newBuilder()
}
@InternalApi
@Provides
@Singleton
fun provideBaseOkHttpClient(
interceptors: DaggerSet<Interceptor>,
cache: Cache
): OkHttpClient {
check(Looper.myLooper() != Looper.getMainLooper()) { "HTTP client initialized on main thread." }
val builder = OkHttpClient.Builder()
builder.interceptors()
.addAll(interceptors)
builder.cache(cache)
return builder.build()
}
@Singleton
@Provides
fun provideCache(context: Context): Cache {
val cacheSize = 5 * 1024 * 1024 // 5 MB
check(Looper.myLooper() != Looper.getMainLooper()) { "Cache initialized on main thread." }
val cacheSize = 10 * 1024 * 1024 // 10 MB
val cacheDir = context.cacheDir
return Cache(cacheDir, cacheSize.toLong())
}
@Provides
@Singleton
fun provideRetrofit(
@InternalApi
okHttpClient: Lazy<OkHttpClient>,
@Named(TVMAZE_BASE_URL) baseUrl: String
): Retrofit {
return Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(MoshiConverterFactory.create())
.callFactory { okHttpClient.get().newCall(it) }
.build()
}
}

View File

@ -0,0 +1,13 @@
package com.android.tvmaze.network
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.Request
import retrofit2.Retrofit
@PublishedApi
internal inline fun Retrofit.Builder.callFactory(
crossinline body: (Request) -> Call
) = callFactory(object : Call.Factory {
override fun newCall(request: Request): Call = body(request)
})

View File

@ -2,25 +2,16 @@ package com.android.tvmaze.network
import dagger.Module
import dagger.Provides
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Named
import javax.inject.Singleton
@Module
object TvMazeApiModule {
@JvmStatic
@Provides
@Singleton
fun provideTvMazeApi(
okHttpClient: OkHttpClient,
@Named(NetworkModule.TVMAZE_BASE_URL) baseUrl: String
retrofit: Retrofit
): TvMazeApi {
return Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(MoshiConverterFactory.create())
.client(okHttpClient)
.build().create(TvMazeApi::class.java)
return retrofit.create(TvMazeApi::class.java)
}
}

View File

@ -6,31 +6,32 @@ import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import androidx.paging.PagedList
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.tvmaze.R
import com.android.tvmaze.base.TvMazeBaseActivity
import com.android.tvmaze.di.ActivityScoped
import com.android.tvmaze.network.home.Show
import kotlinx.android.synthetic.main.activity_all_shows.*
import kotlinx.android.synthetic.main.toolbar.view.*
import javax.inject.Inject
class AllShowsActivity : TvMazeBaseActivity(), ShowsPagedAdaptor.Callback {
@ActivityScoped
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var showsViewModel: ShowsViewModel
private val showsViewModel by viewModels<ShowsViewModel> { viewModelFactory }
private lateinit var showsPagedAdaptor: ShowsPagedAdaptor
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_all_shows)
setToolbar()
showsViewModel = ViewModelProviders.of(this, viewModelFactory).get(ShowsViewModel::class.java)
showsViewModel.onScreenCreated()
initAdapter()
showsViewModel.initialLoadState().observe(this, Observer { setProgress(it) })

View File

@ -0,0 +1,16 @@
package com.android.tvmaze.shows
import androidx.lifecycle.ViewModel
import com.android.tvmaze.di.ViewModelFactoryBindingModule
import com.android.tvmaze.di.ViewModelKey
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
@Module(includes = [ViewModelFactoryBindingModule::class])
abstract class ShowsModule {
@Binds
@IntoMap
@ViewModelKey(ShowsViewModel::class)
abstract fun bindShowsViewModel(showsViewModel: ShowsViewModel): ViewModel
}

View File

@ -6,6 +6,7 @@ import com.android.tvmaze.network.TvMazeApi
import com.android.tvmaze.utils.LiveDataTestUtil
import com.android.tvmaze.utils.MainCoroutineRule
import com.android.tvmaze.utils.TestUtil
import com.android.tvmaze.utils.runBlockingTest
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -32,8 +33,10 @@ class HomeViewModelTest {
@Mock
private lateinit var tvMazeApi: TvMazeApi
@Mock
private lateinit var favoriteShowsRepository: FavoriteShowsRepository
@InjectMocks
private lateinit var homeViewModel: HomeViewModel
@ -44,7 +47,7 @@ class HomeViewModelTest {
@Test
fun testHomeIsLoadedWithShowsWithoutFavorites() {
mainCoroutineRule.runBlockingTest {
mainCoroutineRule.testDispatcher.runBlockingTest {
// Stubbing network calls with fake episode list
whenever(tvMazeApi.getCurrentSchedule("US", TestUtil.currentDate))
.thenReturn(TestUtil.getFakeEpisodeList())
@ -52,30 +55,27 @@ class HomeViewModelTest {
whenever(favoriteShowsRepository.allFavoriteShowIds())
.thenReturn(emptyList())
// Pause coroutine to listen for loading initial state
mainCoroutineRule.pauseDispatcher()
homeViewModel.onScreenCreated()
// Check if status is loading
assertThat(LiveDataTestUtil.getValue(homeViewModel.getHomeViewState())).isEqualTo(Loading)
// Resume coroutine dispatcher to execute pending coroutine actions
mainCoroutineRule.resumeDispatcher()
// Observe on home view state live data
val homeViewState = LiveDataTestUtil.getValue(homeViewModel.getHomeViewState())
// Check for success data
assertThat(homeViewState is Success).isTrue()
val homeViewData = (homeViewState as Success).homeViewData
assertThat(homeViewData.episodes).isNotEmpty()
// compare the response with fake list
assertThat(homeViewData.episodes).hasSize(TestUtil.getFakeEpisodeList().size)
// compare the data and also order
assertThat(homeViewData.episodes).containsExactlyElementsIn(
TestUtil.getFakeEpisodeViewDataList(
false
)
).inOrder()
LiveDataTestUtil.getValue(homeViewModel.getHomeViewState()) {
when (it) {
is Loading -> assertThat(it).isNotNull()
is Success -> {
assertThat(it.homeViewData).isNotNull()
val episodes = it.homeViewData.episodes
assertThat(episodes.isNotEmpty())
// compare the response with fake list
assertThat(episodes).hasSize(TestUtil.getFakeEpisodeList().size)
// compare the data and also order
assertThat(episodes).containsExactlyElementsIn(
TestUtil.getFakeEpisodeViewDataList(
false
)
).inOrder()
}
}
}
}
}
@ -89,30 +89,26 @@ class HomeViewModelTest {
whenever(favoriteShowsRepository.allFavoriteShowIds())
.thenReturn(arrayListOf(1, 2))
// Pause coroutine to listen for loading initial state
mainCoroutineRule.pauseDispatcher()
homeViewModel.onScreenCreated()
// Check if status is loading
assertThat(LiveDataTestUtil.getValue(homeViewModel.getHomeViewState())).isEqualTo(Loading)
// Resume coroutine dispatcher to execute pending coroutine actions
mainCoroutineRule.resumeDispatcher()
// Observe on home view state live data
val homeViewState = LiveDataTestUtil.getValue(homeViewModel.getHomeViewState())
// Check for success data
assertThat(homeViewState is Success).isTrue()
val homeViewData = (homeViewState as Success).homeViewData
assertThat(homeViewData.episodes).isNotEmpty()
// compare the response with fake list
assertThat(homeViewData.episodes).hasSize(TestUtil.getFakeEpisodeList().size)
// compare the data and also order
assertThat(homeViewData.episodes).containsExactlyElementsIn(
TestUtil.getFakeEpisodeViewDataList(
true
)
).inOrder()
LiveDataTestUtil.getValue(homeViewModel.getHomeViewState()) {
when (it) {
is Loading -> assertThat(it).isNotNull()
is Success -> {
assertThat(it.homeViewData).isNotNull()
val episodes = it.homeViewData.episodes
assertThat(episodes.isNotEmpty())
// compare the response with fake list
assertThat(episodes).hasSize(TestUtil.getFakeEpisodeList().size)
// compare the data and also order
assertThat(episodes).containsExactlyElementsIn(
TestUtil.getFakeEpisodeViewDataList(
true
)
).inOrder()
}
}
}
}
}
@ -126,20 +122,13 @@ class HomeViewModelTest {
whenever(favoriteShowsRepository.allFavoriteShowIds())
.thenReturn(arrayListOf(1, 2))
// Pause coroutine to listen for loading initial state
mainCoroutineRule.pauseDispatcher()
homeViewModel.onScreenCreated()
// Check if status is loading
assertThat(LiveDataTestUtil.getValue(homeViewModel.getHomeViewState())).isEqualTo(Loading)
// Resume coroutine dispatcher to execute pending coroutine actions
mainCoroutineRule.resumeDispatcher()
// Observe on home view state live data
val homeViewState = LiveDataTestUtil.getValue(homeViewModel.getHomeViewState())
// Check for success data
assertThat(homeViewState is NetworkError).isTrue()
LiveDataTestUtil.getValue(homeViewModel.getHomeViewState()) {
when (it) {
is Loading -> assertThat(it).isNotNull()
is NetworkError -> assertThat(it.message).isNotEmpty()
}
}
}
}
}

View File

@ -30,4 +30,14 @@ object LiveDataTestUtil {
return data
}
fun <T> getValue(
liveData: LiveData<T>,
callback: (T) -> Unit = {}
) {
val observer = Observer<T> { o ->
callback.invoke(o)
}
liveData.observeForever(observer)
}
}

View File

@ -1,48 +1,39 @@
package com.android.tvmaze.utils
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import kotlin.coroutines.ContinuationInterceptor
/**
* Sets the main coroutines dispatcher to a [TestCoroutineScope] for unit testing. A
* [TestCoroutineScope] provides control over the execution of coroutines.
*
* Declare it as a JUnit Rule:
*
* ```
* @get:Rule
* var mainCoroutineRule = MainCoroutineRule()
* ```
*
* Use it directly as a [TestCoroutineScope]:
*
* ```
* mainCoroutineRule.pauseDispatcher()
* ...
* mainCoroutineRule.resumeDispatcher()
* ...
* mainCoroutineRule.runBlockingTest { }
* ...
*
* ```
*/
@ExperimentalCoroutinesApi
class MainCoroutineRule : TestWatcher(), TestCoroutineScope by TestCoroutineScope() {
class MainCoroutineRule(
val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher() {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(this.coroutineContext[ContinuationInterceptor] as CoroutineDispatcher)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
}
}
@ExperimentalCoroutinesApi
fun MainCoroutineRule.runBlockingTest(block: suspend () -> Unit) =
this.testDispatcher.runBlockingTest {
block()
}

View File

@ -4,45 +4,47 @@ ext.versions = [:]
def versions = [:]
//sdk versions
versions.compile_sdk = 28
versions.min_sdk = 16
versions.min_sdk = 21
versions.target_sdk = 26
versions.app_version_code = 103
versions.app_version_name = '2.0.1'
// android and google essentials
versions.android_plugin = '3.5.3'
versions.android_plugin = '4.0.0'
versions.constraint_layout = '2.0.0-beta4'
versions.google_services = '3.0.0'
versions.lifecycle = '2.1.0'
versions.google_services = '4.3.3'
versions.lifecycle = '2.2.0'
versions.support_lib = '27.1.0'
versions.support_x = '1.0.0'
versions.android_test = '1.2.0'
versions.android_test_junit = '1.1.1'
versions.multidex = '2.0.0'
versions.multidex = '2.0.1'
versions.espresso = '3.2.0'
versions.dagger = '2.24'
versions.dagger = '2.28'
versions.glide = '4.9.0'
versions.junit = '4.12'
versions.mockito = '3.2.4'
versions.okhttp = '4.3.1'
versions.retrofit = '2.6.0'
versions.rxjava = '2.2.10'
versions.rxandroid = '2.1.1'
versions.rxbinding = '2.0.0'
versions.okhttp = '4.7.2'
versions.retrofit = '2.9.0'
versions.findbugs = '3.0.1'
versions.paging = '2.1.0'
versions.room = '2.1.0'
versions.kotlin = '1.3.61'
versions.room = '2.2.5'
versions.kotlin = '1.3.72'
versions.timber = '4.7.1'
versions.mockito_kotlin = '2.2.0'
versions.arch_core_testing = '2.0.0'
versions.robolectric = '4.3.1'
versions.moshi = '1.9.2'
versions.coroutines = '1.3.3'
versions.coroutines_test = '1.3.3'
versions.truth = '1.0'
versions.coroutines = '1.3.7'
versions.coroutines_test = '1.3.7'
versions.truth = '1.0.1'
versions.annotation = '1.1.0'
versions.appcompat = '1.1.0'
versions.chucker = '3.2.0'
versions.ktx_core = '1.2.0'
versions.ktx_collections = '1.1.0'
versions.ktx_fragment = '1.2.4'
versions.ktx_activity = '1.1.0'
ext.versions = versions
def deps = [:]
@ -54,19 +56,24 @@ deps.constraint_layout = "androidx.constraintlayout:constraintlayout:$versions.c
deps.multidex = "androidx.multidex:multidex:$versions.multidex"
deps.google_services = "com.google.gms:google-services:$versions.google_services"
def support_lib = [:]
support_lib.design = "com.google.android.material:material:$versions.support_x"
support_lib.appcompat = "androidx.appcompat:appcompat:$versions.appcompat"
support_lib.recyclerview = "androidx.recyclerview:recyclerview:$versions.support_x"
support_lib.cardview = "androidx.cardview:cardview:$versions.support_x"
support_lib.support_annotations = "androidx.annotation:annotation:$versions.annotation"
deps.support_lib = support_lib
def androidx_lib = [:]
androidx_lib.design = "com.google.android.material:material:$versions.support_x"
androidx_lib.appcompat = "androidx.appcompat:appcompat:$versions.appcompat"
androidx_lib.recyclerview = "androidx.recyclerview:recyclerview:$versions.support_x"
androidx_lib.cardview = "androidx.cardview:cardview:$versions.support_x"
androidx_lib.support_annotations = "androidx.annotation:annotation:$versions.annotation"
androidx_lib.ktx_core = "androidx.core:core-ktx:$versions.ktx_core"
androidx_lib.ktx_collections = "androidx.collection:collection-ktx:$versions.ktx_collections"
androidx_lib.ktx_fragment = "androidx.fragment:fragment-ktx:$versions.ktx_fragment"
androidx_lib.ktx_activity = "androidx.activity:activity-ktx:$versions.ktx_activity"
deps.androidx_lib = androidx_lib
def lifecycle = [:]
lifecycle.extensions = "androidx.lifecycle:lifecycle-extensions:$versions.lifecycle"
lifecycle.compiler = "androidx.lifecycle:lifecycle-common-java8:$versions.lifecycle"
lifecycle.viewmodel_ktx = "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.lifecycle"
lifecycle.livedata_ktx = "androidx.lifecycle:lifecycle-livedata-ktx:$versions.lifecycle"
deps.lifecycle = lifecycle
def espresso = [:]
@ -104,14 +111,6 @@ retrofit.main = "com.squareup.retrofit2:retrofit:$versions.retrofit"
retrofit.moshi = "com.squareup.retrofit2:converter-moshi:$versions.retrofit"
deps.retrofit = retrofit
def rxjava = [:]
rxjava.main = "io.reactivex.rxjava2:rxjava:$versions.rxjava"
rxjava.android = "io.reactivex.rxjava2:rxandroid:$versions.rxandroid"
deps.rxjava = rxjava
def rxbinding = [:]
rxbinding.platform = "com.jakewharton.rxbinding2:rxbinding:$versions.rxbinding"
deps.rxbinding = rxbinding
def android_test = [:]
android_test.rules = "androidx.test:rules:$versions.android_test"
@ -134,7 +133,6 @@ deps.paging = paging
def room = [:]
room.runtime = "androidx.room:room-runtime:$versions.room"
room.compiler = "androidx.room:room-compiler:$versions.room"
room.rxjava2 = "androidx.room:room-rxjava2:$versions.room"
room.testing = "androidx.room:room-testing:$versions.room"
room.ktx = "androidx.room:room-ktx:$versions.room"
deps.room = room
@ -166,4 +164,9 @@ coroutines.test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$versions.corou
deps.coroutines = coroutines
deps.truth = "com.google.truth:truth:$versions.truth"
def chucker = [:]
chucker.debug = "com.github.chuckerteam.chucker:library:$versions.chucker"
chucker.release = "com.github.chuckerteam.chucker:library-no-op:$versions.chucker"
deps.chucker = chucker
ext.deps = deps

View File

@ -14,8 +14,6 @@ org.gradle.parallel=true
# Google Play and Firebase dependencies are migrating to androidX. This is to fix the duplicate class resolution build error to use the androidX libraries
android.useAndroidX=true
android.enableJetifier=true
# Enable Robolectric to use the same resources as Android
android.enableUnitTestBinaryResources=true
# All kapt to use workers
kapt.use.worker.api=true
kapt.incremental.apt=true

View File

@ -1,6 +1,6 @@
#Tue Aug 20 23:28:57 IST 2019
#Sun Jun 14 15:55:13 IST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

View File

@ -1,18 +0,0 @@
apply plugin: 'findbugs'
task findbugs(type: FindBugs) {
description 'Find bugs mainly design flaws, bad practices, multithreaded correctness and code vulnerabilities.'
group 'verification'
excludeFilter = file("$project.rootDir/tools/rules-findbugs.xml")
classes = fileTree("$project.buildDir/intermediates/classes/dev/debug/com/android")
source = fileTree('src/main/java')
effort 'max'
reportLevel = "high"
classpath = files()
reports {
xml.enabled = false
html.enabled = true
html.destination file("$project.buildDir/outputs/findbugs/findbugs.html")
}
}