diff --git a/README.md b/README.md index 8964dfd..5688154 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,16 @@ My way to MVVM using RxJava with new Android databinding ## Summary * Use [MVVM][1] using [architecture components][6] with to separate Android Framework with a [clean architecture][2] to my domain logic. -* Use [Android Databinding][3] to glue view model and Android +* Use [Android Databinding][3] wih [LiveData][8] to glue [ViewModel][9] and Android * Asynchronous communications implemented with [Rx][4]. * Rest API from [ComicVine][5] +* Store data using [Room][7] ## Dependencies * architecture components + * livedata + * room + * viewmodel * rx-java * floating search * okhttp @@ -22,8 +26,6 @@ TODO LIST * Better UI, with Material Design concepts and so on * Add unit tests, allways fail on that :( -* Implement a local datasource with Realm to test it - Developed By ------------ @@ -40,7 +42,7 @@ Fernando Franco Giráldez - License ------- - Copyright 2015 Fernando Franco Giráldez + Copyright 2018 Fernando Franco Giráldez 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 @@ -59,3 +61,6 @@ License [4]: http://reactivex.io/ [5]: http://www.comicvine.com/api/ [6]: https://developer.android.com/topic/libraries/architecture/index.html +[7]: https://developer.android.com/topic/libraries/architecture/room.html +[8]: https://developer.android.com/topic/libraries/architecture/livedata.html +[9]: https://developer.android.com/topic/libraries/architecture/viewmodel.html diff --git a/app/build.gradle b/app/build.gradle index f8f0ce1..ca4158f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -33,7 +33,7 @@ android { dataBinding { enabled = true } - + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -44,11 +44,22 @@ android { } } +kapt { + arguments { + arg("room.schemaLocation", "$projectDir/schemas".toString()) + } +} + + dependencies { kapt libs.databinding_compiler + kapt libs.arch_comp_room_compiler + kaptTest libs.arch_comp_room_compiler implementation libs.arch_comp_livedata implementation libs.arch_comp_viewmodel + implementation libs.arch_comp_room + implementation libs.arch_comp_room_rxjava implementation libs.constraint implementation libs.design implementation libs.floating_search @@ -64,7 +75,10 @@ dependencies { implementation libs.retrofit_rx_java implementation libs.rx_java implementation libs.rx_android + implementation libs.steho + testImplementation libs.arch_comp_room_test + testImplementation libs.arch_comp_test testImplementation libs.junit testImplementation libs.koin_test testImplementation libs.mockito_kotlin diff --git a/app/schemas/es.ffgiraldez.comicsearch.comics.store.ComicDatabase/1.json b/app/schemas/es.ffgiraldez.comicsearch.comics.store.ComicDatabase/1.json new file mode 100644 index 0000000..fc938c2 --- /dev/null +++ b/app/schemas/es.ffgiraldez.comicsearch.comics.store.ComicDatabase/1.json @@ -0,0 +1,217 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "843b5908f7967726e53a210136b5cda2", + "entities": [ + { + "tableName": "queries", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query_identifier` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `search_term` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "queryId", + "columnName": "query_identifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "searchTerm", + "columnName": "search_term", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "query_identifier" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "idx_query_identifier", + "unique": false, + "columnNames": [ + "query_identifier" + ], + "createSql": "CREATE INDEX `idx_query_identifier` ON `${TABLE_NAME}` (`query_identifier`)" + }, + { + "name": "idx_query_term", + "unique": false, + "columnNames": [ + "search_term" + ], + "createSql": "CREATE INDEX `idx_query_term` ON `${TABLE_NAME}` (`search_term`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "suggestions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`suggestionId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query_id` INTEGER NOT NULL, `title` TEXT NOT NULL, FOREIGN KEY(`query_id`) REFERENCES `queries`(`query_identifier`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "suggestionId", + "columnName": "suggestionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "queryId", + "columnName": "query_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "suggestionId" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "idx_suggestion_id", + "unique": false, + "columnNames": [ + "query_id" + ], + "createSql": "CREATE INDEX `idx_suggestion_id` ON `${TABLE_NAME}` (`query_id`)" + } + ], + "foreignKeys": [ + { + "table": "queries", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "query_id" + ], + "referencedColumns": [ + "query_identifier" + ] + } + ] + }, + { + "tableName": "search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search_identifier` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `search_term` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "queryId", + "columnName": "search_identifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "searchTerm", + "columnName": "search_term", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "search_identifier" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "idx_search_identifier", + "unique": false, + "columnNames": [ + "search_identifier" + ], + "createSql": "CREATE INDEX `idx_search_identifier` ON `${TABLE_NAME}` (`search_identifier`)" + }, + { + "name": "idx_search_term", + "unique": false, + "columnNames": [ + "search_term" + ], + "createSql": "CREATE INDEX `idx_search_term` ON `${TABLE_NAME}` (`search_term`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "volumes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`suggestionId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `search_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `url` TEXT NOT NULL, FOREIGN KEY(`search_id`) REFERENCES `search`(`search_identifier`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "suggestionId", + "columnName": "suggestionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "queryId", + "columnName": "search_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "suggestionId" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "idx_search_id", + "unique": false, + "columnNames": [ + "search_id" + ], + "createSql": "CREATE INDEX `idx_search_id` ON `${TABLE_NAME}` (`search_id`)" + } + ], + "foreignKeys": [ + { + "table": "search", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "search_id" + ], + "referencedColumns": [ + "search_identifier" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"843b5908f7967726e53a210136b5cda2\")" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/es/ffgiraldez/comicsearch/ComicApplication.kt b/app/src/main/java/es/ffgiraldez/comicsearch/ComicApplication.kt index 8735d47..8ade568 100644 --- a/app/src/main/java/es/ffgiraldez/comicsearch/ComicApplication.kt +++ b/app/src/main/java/es/ffgiraldez/comicsearch/ComicApplication.kt @@ -1,12 +1,14 @@ package es.ffgiraldez.comicsearch import android.app.Application +import com.facebook.stetho.Stetho import es.ffgiraldez.comicsearch.di.comicContext import org.koin.android.ext.android.startKoin class ComicApplication : Application() { override fun onCreate() { super.onCreate() - startKoin(this, listOf(comicContext)); + startKoin(this, listOf(comicContext)) + Stetho.initializeWithDefaults(this) } } \ No newline at end of file diff --git a/app/src/main/java/es/ffgiraldez/comicsearch/comics/ComicRepository.kt b/app/src/main/java/es/ffgiraldez/comicsearch/comics/ComicRepository.kt index 107512e..f9718a7 100644 --- a/app/src/main/java/es/ffgiraldez/comicsearch/comics/ComicRepository.kt +++ b/app/src/main/java/es/ffgiraldez/comicsearch/comics/ComicRepository.kt @@ -1,29 +1,86 @@ package es.ffgiraldez.comicsearch.comics +import android.util.Log import es.ffgiraldez.comicsearch.comics.data.ComicVineApi -import io.reactivex.Single +import es.ffgiraldez.comicsearch.comics.store.ComicDatabase +import es.ffgiraldez.comicsearch.comics.store.QueryEntity +import es.ffgiraldez.comicsearch.comics.store.SearchEntity +import io.reactivex.Completable +import io.reactivex.Flowable import io.reactivex.schedulers.Schedulers class ComicRepository( - private val api: ComicVineApi + private val api: ComicVineApi, + private val database: ComicDatabase ) { - fun searchSuggestion(query: String): Single> = api.fetchSuggestedVolumes(query) - .subscribeOn(Schedulers.io()) - .map { response -> - response.results - .distinctBy { it.name } - .map { it.name } - } - .subscribeOn(Schedulers.computation()) - - fun searchVolume(query: String): Single> = api.fetchVolumes(query) - .subscribeOn(Schedulers.io()) - .map { response -> - response.results - .filter { it.apiPublisher != null && it.apiImage != null } - .map { - Volume(it.name, it.apiPublisher!!.name, it.apiImage!!.url) + + fun findSuggestion(term: String): Flowable> = + database.suggestionDao().findQueryByTerm(term) + .flatMap { + when (it.isEmpty()) { + true -> searchSuggestionsOnApi(term) + false -> searchSuggestionsOnDatabase(it.first()) + } + } + + fun findVolume(term: String): Flowable> = + database.volumeDao().findQueryByTerm(term) + .flatMap { + when (it.isEmpty()) { + true -> searchVolumeOnApi(term) + false -> searchVolumeOnDatabase(it.first()) + } + } + + private fun searchSuggestionsOnApi(term: String): Flowable> = + api.fetchSuggestedVolumes(term) + .subscribeOn(Schedulers.io()) + .doOnSubscribe { Log.d("cambio", "pidiendo findSuggestion la api") } + .map { response -> + response.results + .distinctBy { it.name } + .map { it.name } + } + .flatMapCompletable { response -> + Completable.fromAction { + Log.d("cambio", "guardando query: $term en bd") + database.suggestionDao().insert( + term, + response + ) } - } - .subscribeOn(Schedulers.computation()) + }.toFlowable>() + + private fun searchSuggestionsOnDatabase(query: QueryEntity): Flowable> = + database.suggestionDao().findSuggestionByQuery(query.queryId) + .doOnSubscribe { Log.d("cambio", "devolviendo resultados") } + .subscribeOn(Schedulers.io()) + .map { suggestions -> suggestions.map { it.title } } + + private fun searchVolumeOnApi(term: String): Flowable> = + api.fetchVolumes(term) + .subscribeOn(Schedulers.io()) + .map { response -> + response.results + .filter { it.apiPublisher != null && it.apiImage != null } + .map { + Volume(it.name, it.apiPublisher!!.name, it.apiImage!!.url) + } + } + .subscribeOn(Schedulers.computation()) + .flatMapCompletable { response -> + Completable.fromAction { + Log.d("cambio", "guardando query: $term en bd") + database.volumeDao().insert( + term, + response + ) + } + }.toFlowable>() + + private fun searchVolumeOnDatabase(query: SearchEntity): Flowable> = + database.volumeDao().findVolumeByQuery(query.queryId) + .doOnSubscribe { Log.d("cambio", "devolviendo resultados") } + .subscribeOn(Schedulers.io()) + .map { volumeList -> volumeList.map { Volume(it.title, it.author, it.url) } } } \ No newline at end of file diff --git a/app/src/main/java/es/ffgiraldez/comicsearch/comics/store/ComicDao.kt b/app/src/main/java/es/ffgiraldez/comicsearch/comics/store/ComicDao.kt new file mode 100644 index 0000000..67772fe --- /dev/null +++ b/app/src/main/java/es/ffgiraldez/comicsearch/comics/store/ComicDao.kt @@ -0,0 +1,58 @@ +package es.ffgiraldez.comicsearch.comics.store + +import android.arch.persistence.room.Dao +import android.arch.persistence.room.Insert +import android.arch.persistence.room.Query +import android.arch.persistence.room.Transaction +import es.ffgiraldez.comicsearch.comics.Volume +import io.reactivex.Flowable + +@Dao +abstract class SuggestionDao { + + @Query("SELECT * FROM queries WHERE search_term like :searchTerm") + abstract fun findQueryByTerm(searchTerm: String): Flowable> + + @Query("SELECT * FROM suggestions WHERE query_id = :queryId") + abstract fun findSuggestionByQuery(queryId: Long): Flowable> + + @Insert + abstract fun insert(query: QueryEntity): Long + + @Insert + abstract fun insert(vararg suggestions: SuggestionEntity) + + @Transaction + @Insert + fun insert(query: String, volumeTitles: List) { + val id = insert(QueryEntity(0, query)) + val suggestions = volumeTitles.map { SuggestionEntity(0, id, it) } + insert(*suggestions.toTypedArray()) + + } +} + +@Dao +abstract class VolumeDao { + + @Query("SELECT * FROM search WHERE search_term like :searchTerm") + abstract fun findQueryByTerm(searchTerm: String): Flowable> + + @Query("SELECT * FROM volumes WHERE search_id = :queryId") + abstract fun findVolumeByQuery(queryId: Long): Flowable> + + @Insert + abstract fun insert(query: SearchEntity): Long + + @Insert + abstract fun insert(vararg suggestions: VolumeEntity) + + @Transaction + @Insert + fun insert(query: String, volumeTitles: List) { + val id = insert(SearchEntity(0, query)) + val suggestions = volumeTitles.map { VolumeEntity(0, id, it.title, it.author, it.cover) } + insert(*suggestions.toTypedArray()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/es/ffgiraldez/comicsearch/comics/store/ComicDatabase.kt b/app/src/main/java/es/ffgiraldez/comicsearch/comics/store/ComicDatabase.kt new file mode 100644 index 0000000..17768bc --- /dev/null +++ b/app/src/main/java/es/ffgiraldez/comicsearch/comics/store/ComicDatabase.kt @@ -0,0 +1,19 @@ +package es.ffgiraldez.comicsearch.comics.store + +import android.arch.persistence.room.Database +import android.arch.persistence.room.RoomDatabase + +@Database( + entities = [ + QueryEntity::class, + SuggestionEntity::class, + SearchEntity::class, + VolumeEntity::class + ], + version = 1 +) +abstract class ComicDatabase : RoomDatabase() { + abstract fun suggestionDao(): SuggestionDao + + abstract fun volumeDao(): VolumeDao +} \ No newline at end of file diff --git a/app/src/main/java/es/ffgiraldez/comicsearch/comics/store/ComicEntities.kt b/app/src/main/java/es/ffgiraldez/comicsearch/comics/store/ComicEntities.kt new file mode 100644 index 0000000..0a676e4 --- /dev/null +++ b/app/src/main/java/es/ffgiraldez/comicsearch/comics/store/ComicEntities.kt @@ -0,0 +1,101 @@ +package es.ffgiraldez.comicsearch.comics.store + +import android.arch.persistence.room.* +import android.arch.persistence.room.ForeignKey.CASCADE + +@Entity( + tableName = "queries", + indices = [ + Index( + value = ["query_identifier"], + name = "idx_query_identifier" + ), + Index( + value = ["search_term"], + name = "idx_query_term" + ) + ] +) +data class QueryEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "query_identifier") + var queryId: Long, + @ColumnInfo(name = "search_term") + var searchTerm: String +) + +@Entity( + tableName = "suggestions", + indices = [ + Index( + value = ["query_id"], + name = "idx_suggestion_id" + ) + ], + foreignKeys = [ + ForeignKey( + entity = QueryEntity::class, + parentColumns = ["query_identifier"], + childColumns = ["query_id"], + onDelete = CASCADE + ) + ] +) +data class SuggestionEntity( + @PrimaryKey(autoGenerate = true) + var suggestionId: Long, + @ColumnInfo(name = "query_id") + var queryId: Long, + @ColumnInfo(name = "title") + var title: String +) + +@Entity( + tableName = "search", + indices = [ + Index( + value = ["search_identifier"], + name = "idx_search_identifier" + ), + Index( + value = ["search_term"], + name = "idx_search_term" + ) + ] +) +data class SearchEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "search_identifier") + var queryId: Long, + @ColumnInfo(name = "search_term") + var searchTerm: String +) +@Entity( + tableName = "volumes", + indices = [ + Index( + value = ["search_id"], + name = "idx_search_id" + ) + ], + foreignKeys = [ + ForeignKey( + entity = SearchEntity::class, + parentColumns = ["search_identifier"], + childColumns = ["search_id"], + onDelete = CASCADE + ) + ] +) +data class VolumeEntity( + @PrimaryKey(autoGenerate = true) + var suggestionId: Long, + @ColumnInfo(name = "search_id") + var queryId: Long, + @ColumnInfo(name = "title") + var title: String, + @ColumnInfo(name = "author") + var author: String, + @ColumnInfo(name = "url") + var url: String +) \ No newline at end of file diff --git a/app/src/main/java/es/ffgiraldez/comicsearch/di/ApplicationContext.kt b/app/src/main/java/es/ffgiraldez/comicsearch/di/ApplicationContext.kt index 9caa87c..1b6b274 100644 --- a/app/src/main/java/es/ffgiraldez/comicsearch/di/ApplicationContext.kt +++ b/app/src/main/java/es/ffgiraldez/comicsearch/di/ApplicationContext.kt @@ -1,18 +1,22 @@ package es.ffgiraldez.comicsearch.di +import android.arch.persistence.room.Room import es.ffgiraldez.comicsearch.comics.ComicRepository import es.ffgiraldez.comicsearch.comics.data.ComicVineApi +import es.ffgiraldez.comicsearch.comics.store.ComicDatabase import es.ffgiraldez.comicsearch.navigation.Navigator import es.ffgiraldez.comicsearch.search.presentation.SearchViewModel import es.ffgiraldez.comicsearch.sugestion.presentation.SuggestionViewModel import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor +import org.koin.dsl.context.ParameterProvider import org.koin.dsl.module.applicationContext import retrofit2.Retrofit import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory const val ACTIVITY_PARAM: String = "activity" +const val CONTEXT_PARAM: String = "context" val comicContext = applicationContext { factory { @@ -29,8 +33,9 @@ val comicContext = applicationContext { .build() .create(ComicVineApi::class.java) } - factory { ComicRepository(get()) } - factory { SuggestionViewModel(get()) } + factory { ComicRepository(get(), get({ it.values })) } + factory { SuggestionViewModel(get({ it.values })) } factory { SearchViewModel(get()) } factory { params -> Navigator(params[ACTIVITY_PARAM]) } + bean { params: ParameterProvider -> Room.databaseBuilder(params[CONTEXT_PARAM], ComicDatabase::class.java, "comics").build() } } \ No newline at end of file diff --git a/app/src/main/java/es/ffgiraldez/comicsearch/platform/LiveDataExt.kt b/app/src/main/java/es/ffgiraldez/comicsearch/platform/LiveDataExt.kt index bc1d7de..437fa30 100644 --- a/app/src/main/java/es/ffgiraldez/comicsearch/platform/LiveDataExt.kt +++ b/app/src/main/java/es/ffgiraldez/comicsearch/platform/LiveDataExt.kt @@ -2,15 +2,17 @@ package es.ffgiraldez.comicsearch.platform import android.arch.lifecycle.LiveData import android.arch.lifecycle.Observer +import io.reactivex.BackpressureStrategy +import io.reactivex.Flowable import io.reactivex.Observable import io.reactivex.android.MainThreadDisposable -fun LiveData.toObservable(): Observable = - Observable.create { emitter -> +fun LiveData.toFlowable(): Flowable = + Flowable.create({ emitter -> val observer = Observer { it?.let { emitter.onNext(it) } } - this@toObservable.observeForever(observer) + observeForever(observer) emitter.setCancellable { object : MainThreadDisposable() { @@ -18,4 +20,4 @@ fun LiveData.toObservable(): Observable = override fun onDispose() = removeObserver(observer) } } - } \ No newline at end of file + }, BackpressureStrategy.LATEST) \ No newline at end of file diff --git a/app/src/main/java/es/ffgiraldez/comicsearch/search/presentation/SearchViewModel.kt b/app/src/main/java/es/ffgiraldez/comicsearch/search/presentation/SearchViewModel.kt index 96aeb71..eb00cc2 100644 --- a/app/src/main/java/es/ffgiraldez/comicsearch/search/presentation/SearchViewModel.kt +++ b/app/src/main/java/es/ffgiraldez/comicsearch/search/presentation/SearchViewModel.kt @@ -5,7 +5,7 @@ import android.arch.lifecycle.ViewModel import android.util.Log import es.ffgiraldez.comicsearch.comics.ComicRepository import es.ffgiraldez.comicsearch.comics.Volume -import es.ffgiraldez.comicsearch.platform.toObservable +import es.ffgiraldez.comicsearch.platform.toFlowable class SearchViewModel( private val repo: ComicRepository @@ -16,12 +16,12 @@ class SearchViewModel( val results: MutableLiveData> = MutableLiveData() init { - query.toObservable() + loading.value = false + results.value = emptyList() + query.toFlowable() .doOnNext { loading.postValue(true) } .doOnNext { results.postValue(emptyList()) } - .switchMapSingle { repo.searchVolume(it) } - .doOnSubscribe { loading.value = false } - .doOnSubscribe { results.value = emptyList() } + .switchMap { repo.findVolume(it) } .doOnNext { loading.postValue(false) } .subscribe { results.postValue(it) diff --git a/app/src/main/java/es/ffgiraldez/comicsearch/search/ui/SearchActivity.kt b/app/src/main/java/es/ffgiraldez/comicsearch/search/ui/SearchActivity.kt index 2163ffe..912d15b 100644 --- a/app/src/main/java/es/ffgiraldez/comicsearch/search/ui/SearchActivity.kt +++ b/app/src/main/java/es/ffgiraldez/comicsearch/search/ui/SearchActivity.kt @@ -6,6 +6,7 @@ import android.support.v7.app.AppCompatActivity import es.ffgiraldez.comicsearch.R import es.ffgiraldez.comicsearch.databinding.SearchActivityBinding import es.ffgiraldez.comicsearch.di.ACTIVITY_PARAM +import es.ffgiraldez.comicsearch.di.CONTEXT_PARAM import es.ffgiraldez.comicsearch.navigation.Navigator import es.ffgiraldez.comicsearch.search.presentation.SearchViewModel import es.ffgiraldez.comicsearch.sugestion.presentation.SuggestionViewModel @@ -14,8 +15,8 @@ import org.koin.android.ext.android.inject class SearchActivity : AppCompatActivity() { - private val suggestionViewModel by viewModel() - private val searchViewModel by viewModel() + private val suggestionViewModel by viewModel(parameters = { mapOf(CONTEXT_PARAM to this.applicationContext) }) + private val searchViewModel by viewModel(parameters = { mapOf(CONTEXT_PARAM to this.applicationContext) }) private val navigator by inject(parameters = { mapOf(ACTIVITY_PARAM to this) }) override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/es/ffgiraldez/comicsearch/search/ui/SearchScreenDelegate.kt b/app/src/main/java/es/ffgiraldez/comicsearch/search/ui/SearchScreenDelegate.kt index 4457640..edb0f46 100644 --- a/app/src/main/java/es/ffgiraldez/comicsearch/search/ui/SearchScreenDelegate.kt +++ b/app/src/main/java/es/ffgiraldez/comicsearch/search/ui/SearchScreenDelegate.kt @@ -12,9 +12,8 @@ class SearchScreenDelegate( val adapter: SearchVolumeAdapter, private val navigator: Navigator ) { - fun onVolumeSelected(volume: Volume) { - navigator.to(Screen.Detail(volume)) - } + fun onVolumeSelected(volume: Volume) = + navigator.to(Screen.Detail(volume)) fun onQueryChange(new: String) = with(suggestions) { query.value = new } diff --git a/app/src/main/java/es/ffgiraldez/comicsearch/sugestion/presentation/SuggestionViewModel.kt b/app/src/main/java/es/ffgiraldez/comicsearch/sugestion/presentation/SuggestionViewModel.kt index f42bb55..261934a 100644 --- a/app/src/main/java/es/ffgiraldez/comicsearch/sugestion/presentation/SuggestionViewModel.kt +++ b/app/src/main/java/es/ffgiraldez/comicsearch/sugestion/presentation/SuggestionViewModel.kt @@ -4,7 +4,7 @@ import android.arch.lifecycle.MutableLiveData import android.arch.lifecycle.ViewModel import android.util.Log import es.ffgiraldez.comicsearch.comics.ComicRepository -import es.ffgiraldez.comicsearch.platform.toObservable +import es.ffgiraldez.comicsearch.platform.toFlowable import java.util.concurrent.TimeUnit class SuggestionViewModel( @@ -16,13 +16,13 @@ class SuggestionViewModel( val results: MutableLiveData> = MutableLiveData() init { - query.toObservable() + loading.value = false + results.value = emptyList() + query.toFlowable() .debounce(400, TimeUnit.MILLISECONDS) .doOnNext { loading.postValue(true) } .doOnNext { results.postValue(emptyList()) } - .switchMapSingle { repo.searchSuggestion(it) } - .doOnSubscribe { loading.value = false } - .doOnSubscribe { results.value = emptyList() } + .switchMap { repo.findSuggestion(it) } .doOnNext { loading.postValue(false) } .subscribe { Log.d("cambio", "$it") diff --git a/app/src/test/java/es/ffgiraldez/comicsearch/di/TestContextResolution.kt b/app/src/test/java/es/ffgiraldez/comicsearch/di/TestContextResolution.kt index 96db3d1..62e3448 100644 --- a/app/src/test/java/es/ffgiraldez/comicsearch/di/TestContextResolution.kt +++ b/app/src/test/java/es/ffgiraldez/comicsearch/di/TestContextResolution.kt @@ -1,18 +1,30 @@ package es.ffgiraldez.comicsearch.di +import android.app.Activity +import android.arch.core.executor.testing.InstantTaskExecutorRule import android.content.Context import com.nhaarman.mockito_kotlin.mock +import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import org.koin.standalone.StandAloneContext.startKoin import org.koin.test.KoinTest import org.koin.test.dryRun class TestContextResolution : KoinTest { + + @get:Rule + var rule: TestRule = InstantTaskExecutorRule() @Test fun `dry run`() { // start Koin startKoin(listOf(comicContext)) // dry run of given module list - dryRun(defaultParameters = { mapOf(ACTIVITY_PARAM to mock {}) }) + dryRun(defaultParameters = { + mapOf( + ACTIVITY_PARAM to mock(), + CONTEXT_PARAM to mock() + ) + }) } } \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index c387781..a9e13a1 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -7,13 +7,14 @@ ext { target_sdk = 27 versions = [ - architecture_components: '1.1.1', - android_plugin : '3.1.0', - koin : '0.9.1', - kotlin : '1.2.31', - okhttp : '3.9.1', - retrofit : '2.4.0', - support : '27.1.0', + arch_comp_lifecycle : '1.1.1', + arch_comp_persistence: '1.0.0', + android_plugin : '3.1.1', + koin : '0.9.1', + kotlin : '1.2.31', + okhttp : '3.9.1', + retrofit : '2.4.0', + support : '27.1.0', ] build_plugins = [ @@ -22,28 +23,33 @@ ext { ] libs = [ - arch_comp_livedata : [group: 'android.arch.lifecycle', name: 'livedata', version: versions.architecture_components], - arch_comp_viewmodel : [group: 'android.arch.lifecycle', name: 'viewmodel', version: versions.architecture_components], - arch_comp_reactive : [group: 'android.arch.lifecycle', name: 'reactivestreams', version: versions.architecture_components], - constraint : [group: 'com.android.support.constraint', name: 'constraint-layout', version: '1.0.2'], - databinding_compiler: [group: 'com.android.databinding', name: 'compiler', version: versions.android_plugin], - design : [group: 'com.android.support', name: 'design', version: versions.support], - floating_search : [group: 'com.github.arimorty', name: 'floatingsearchview', version: '2.1.1'], - junit : [group: 'junit', name: 'junit', version: '4.12'], - okhttp : [group: 'com.squareup.okhttp3', name: 'okhttp', version: versions.okhttp], - okhttp_logging : [group: 'com.squareup.okhttp3', name: 'logging-interceptor', version: versions.okhttp], - koin : [group: 'org.koin', name: 'koin-core', version: versions.koin], - koin_android : [group: 'org.koin', name: 'koin-android', version: versions.koin], - koin_architecture : [group: 'org.koin', name: 'koin-android-architecture', version: versions.koin], - koin_test : [group: 'org.koin', name: 'koin-test', version: versions.koin], - kotlin_stdlib : [group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk7', version: versions.kotlin], - mockito_kotlin : [group: 'com.nhaarman', name: 'mockito-kotlin', version: '1.5.0'], - picasso : [group: 'com.squareup.picasso', name: 'picasso', version: '2.5.2'], - retrofit : [group: 'com.squareup.retrofit2', name: 'retrofit', version: versions.retrofit], - retrofit_gson : [group: 'com.squareup.retrofit2', name: 'converter-gson', version: versions.retrofit], - retrofit_rx_java : [group: 'com.squareup.retrofit2', name: 'adapter-rxjava2', version: versions.retrofit], - rx_java : [group: 'io.reactivex.rxjava2', name: 'rxjava', version: '2.1.10'], - rx_android : [group: 'io.reactivex.rxjava2', name: 'rxandroid', version: '2.0.2'] + arch_comp_livedata : [group: 'android.arch.lifecycle', name: 'livedata', version: versions.arch_comp_lifecycle], + arch_comp_viewmodel : [group: 'android.arch.lifecycle', name: 'viewmodel', version: versions.arch_comp_lifecycle], + arch_comp_room : [group: 'android.arch.persistence.room', name: 'runtime', version: versions.arch_comp_persistence], + arch_comp_room_compiler: [group: 'android.arch.persistence.room', name: 'compiler', version: versions.arch_comp_persistence], + arch_comp_room_rxjava : [group: 'android.arch.persistence.room', name: 'rxjava2', version: versions.arch_comp_persistence], + arch_comp_room_test : [group: 'android.arch.persistence.room', name: 'testing', version: versions.arch_comp_persistence], + arch_comp_test : [group: 'android.arch.core', name: 'core-testing', version: versions.arch_comp_lifecycle], + constraint : [group: 'com.android.support.constraint', name: 'constraint-layout', version: '1.0.2'], + databinding_compiler : [group: 'com.android.databinding', name: 'compiler', version: versions.android_plugin], + design : [group: 'com.android.support', name: 'design', version: versions.support], + floating_search : [group: 'com.github.arimorty', name: 'floatingsearchview', version: '2.1.1'], + junit : [group: 'junit', name: 'junit', version: '4.12'], + okhttp : [group: 'com.squareup.okhttp3', name: 'okhttp', version: versions.okhttp], + okhttp_logging : [group: 'com.squareup.okhttp3', name: 'logging-interceptor', version: versions.okhttp], + koin : [group: 'org.koin', name: 'koin-core', version: versions.koin], + koin_android : [group: 'org.koin', name: 'koin-android', version: versions.koin], + koin_architecture : [group: 'org.koin', name: 'koin-android-architecture', version: versions.koin], + koin_test : [group: 'org.koin', name: 'koin-test', version: versions.koin], + kotlin_stdlib : [group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk7', version: versions.kotlin], + mockito_kotlin : [group: 'com.nhaarman', name: 'mockito-kotlin', version: '1.5.0'], + picasso : [group: 'com.squareup.picasso', name: 'picasso', version: '2.5.2'], + retrofit : [group: 'com.squareup.retrofit2', name: 'retrofit', version: versions.retrofit], + retrofit_gson : [group: 'com.squareup.retrofit2', name: 'converter-gson', version: versions.retrofit], + retrofit_rx_java : [group: 'com.squareup.retrofit2', name: 'adapter-rxjava2', version: versions.retrofit], + rx_java : [group: 'io.reactivex.rxjava2', name: 'rxjava', version: '2.1.10'], + rx_android : [group: 'io.reactivex.rxjava2', name: 'rxandroid', version: '2.0.2'], + steho : [group: 'com.facebook.stetho', name: 'stetho', version: '1.5.0'] ] }