Android Room can not create JSON schema for testing migrations - android

I have created migration from 1 to 2 version of my database.
I have the app in a few modules like:
app
data
domain
I have tried adding this into build.gradle of app and data modules:
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
Here is my MigrationTest class:
#RunWith(AndroidJUnit4.class)
public class MigrationTest {
private static final String TEST_DB = "migration-test";
#Rule public MigrationTestHelper helper;
private Profile profile;
public MigrationTest() {
helper = new MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase.class.getCanonicalName(),
new FrameworkSQLiteOpenHelperFactory());
}
#Before
public void setUp(){
profile = createProfile();
}
#Test public void migrate1To2() throws IOException {
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
insertProfile(db);
db.close();
AppDatabase database = (AppDatabase) helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);
Single<ProfileData> profileDataSingle = database.profileDao().getById("userId");
ProfileData profileData = profileDataSingle.blockingGet();
Profile currentProfile = ProfileMapper.transform(profileData);
assertEquals(currentProfile.getUserId(), profile.getUserId());
}
Here is failing test:
java.io.FileNotFoundException: Cannot find the schema file in the
assets folder. Make sure to include the exported json schemas in your
test assert inputs. See
https://developer.android.com/topic/libraries/architecture/room.html#db-migration-testing
for details. Missing file: org.app.app.data.sql.AppDatabase/1.json

For Kotliners:
android{
defaultConfig {
// ...
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
}
sourceSets {
getByName("androidTest"){
assets.srcDirs(File(projectDir, "schemas"))
}
}
}

For me it helps, when I add this (I simply forgot it). Maybe it helps someone
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}

This solution in Kotlin is:
Add the following to build.gradle(app):
android {
defaultConfig {
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
}
sourceSets {
getByName("androidTest") {
assets.srcDirs(files(projectDir, "schemas"))
}
}
dependencies {
implementation 'androidx.room:room-runtime:2.2.3'
kapt 'androidx.room:room-compiler:2.2.3'
annotationProcessor 'androidx.room:room-compiler:2.2.3'
implementation 'androidx.room:room-rxjava2:2.2.3'
androidTestImplementation 'androidx.room:room-testing:2.2.3'
}
P.S: Dont Forget to set exportSchema = true in the Database file.

I had a similar problem to Zookey. All I was doing wrong was looking for a schema file that didn't exist. I was testing migration 8 to 9, but version 8.json wasn't generated. I had to created my database from version 9.json instead, and test from there.

Happens because the json schema for the migration version is not found in schemas folder.
For instance, if you are testing migration from version 1 to 2, the file /schemas/*/1.json must exist.

Related

Actual/Expect classes doesn't work in Kotlin Mutliplatform proyect

I was develoing an KMM App, where I try to implement a regular localDatasource and remoteDatasource, using SQLDelight and Ktor respectively.
My problems comes when I try to shared the native code from AndroidApp and iosMain, into commonModule. I start to get the following error into my commonModule expect class:
Expected function 'cache' has no actual declaration in module KMM-APP.shared for JVM
Expected function 'cache' has no actual declaration in module KMM-APP.shared.iosArm64Main for Native
Expected function 'cache' has no actual declaration in module KMM-APP.shared.iosX64Main for Native
It's a bit confuse, in order I don't make use of jvm module in my proyect, although I do for IOS module.
Here it's my cacheAndroid.kt of AndroidApp module:
import android.content.Context
import com.example.kmp_app.db.PetsDatabase
import com.squareup.sqldelight.android.AndroidSqliteDriver
lateinit var appContext: Context
internal actual fun cache(): PetsDatabase {
val driver = AndroidSqliteDriver(PetsDatabase.Schema, appContext, "petsDB.db")
return PetsDatabase(driver)
}
Here is the classes of my IOS module:
import com.example.kmp_app.db.PetsDatabase
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
internal actual fun cache(): PetsDatabase {
val driver = NativeSqliteDriver(PetsDatabase.Schema, "petsDB.db")
return PetsDatabase(driver)
}
And the use into commonModule:
internal expect fun cache(): PetsDatabase
I in this last line of code where I reciving the error above, but I also get the error into the actual classes of Android and IOS modules, into their expect class variant.
Finally regarding my build.gradle(common)
plugins {
kotlin("multiplatform")
kotlin("native.cocoapods")
id("com.android.library")
id("kotlinx-serialization")
id("com.squareup.sqldelight")
}
version = "1.0"
kotlin {
targets{
ios {
binaries {
framework {
baseName = "shared"
}
}
}
// Block from https://github.com/cashapp/sqldelight/issues/2044#issuecomment-721299517.
val onPhone = System.getenv("SDK_NAME")?.startsWith("iphoneos") ?: false
if (onPhone) {
iosArm64("ios")
} else {
iosX64("ios")
}
android()
//iosSimulatorArm64() sure all ios dependencies support this target
}
cocoapods {
summary = "Some description for the Shared Module"
homepage = "Link to the Shared Module homepage"
ios.deploymentTarget = "14.1"
podfile = project.file("../iosApp/Podfile")
}
sourceSets {
all {
languageSettings.apply {
useExperimentalAnnotation("kotlinx.coroutines.ExperimentalCoroutinesApi")
}
}
val commonMain by getting{
dependencies {
implementation(kotlin("stdlib-common"))
implementation(Coroutines.Core.core)
implementation(Ktor.Core.common)
implementation(Ktor.Json.common)
implementation(Ktor.Logging.common)
implementation(Ktor.Serialization.common)
implementation(SqlDelight.runtime)
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
implementation(Ktor.Mock.common)
}
}
val androidMain by getting{
dependencies {
implementation(kotlin("stdlib"))
implementation(Coroutines.Core.core)
implementation(Ktor.android)
implementation(Ktor.Core.jvm)
implementation(Ktor.Json.jvm)
implementation(Ktor.Logging.jvm)
implementation(Ktor.Logging.slf4j)
implementation(Ktor.Mock.jvm)
implementation(Ktor.Serialization.jvm)
implementation(Serialization.core)
implementation(SqlDelight.android)
}
}
val androidAndroidTestRelease by getting
val androidTest by getting {
dependsOn(androidAndroidTestRelease)
dependencies {
implementation(kotlin("test-junit"))
implementation("junit:junit:4.13.2")
}
}
val iosX64Main by getting
val iosArm64Main by getting
//val iosSimulatorArm64Main by getting
val ios by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
//iosSimulatorArm64Main.dependsOn(this)
dependencies {
implementation(SqlDelight.native)
}
}
}
}
android {
compileSdk = 31
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdk = 21
targetSdk = 31
versionCode = 1
versionName = "1.0"
}
}
sqldelight {
database("PetsDatabase") {
packageName = "com.example.kmp_app.db"
sourceFolders = listOf("sqldelight")
}
}
And my proyect build.gradle:
buildscript {
repositories {
google()
mavenCentral()
jcenter()
}
dependencies {
classpath("com.android.tools.build:gradle:4.2.0")
classpath(kotlin("gradle-plugin", version = Versions.kotlin))
classpath(kotlin("serialization", version = Versions.kotlin))
classpath("com.squareup.sqldelight:gradle-plugin:${Versions.sqldelight}")
}
}
allprojects {
repositories {
google()
mavenCentral()
jcenter()
}
}
plugins{
//kotlin("android") version "${Versions.kotlin}" apply false
}
I hope you can help and if like this, take thanks in advance !
I think it related with packageName in your Gradle:
packageName = "com.example.kmp_app.db"
Try to pass route of your cache function instead of "com.example.kmp_app.db"
like if my cache function exists on dataSource.cacheSource, we will pass "com.example.kmp_app.db.dataSource.cacheSource"
Be sure your Cache actual / expect function have the same package name like this "com.example.kmp_app.db.dataSource.cacheSource"
Shared gradle
sqldelight {
database("RecipeDatabase") {
packageName = "com.example.food1fork.Food1ForkKmm.DataSource.cacheSource"
sourceFolders = listOf("SqlDelight")
}
}
iOS module
package com.example.food1fork.Food1ForkKmm.DataSource.cacheSource
actual class DriverFactory {
actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(RecipeDatabase.Schema, "recipes.db")
}
}
Android module
package com.example.food1fork.Food1ForkKmm.DataSource.cacheSource
actual class DriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver {
return AndroidSqliteDriver(RecipeDatabase.Schema, context, "recipes.db")
}
}
Shared module
package com.example.food1fork.Food1ForkKmm.DataSource.cacheSource
expect class DriverFactory {
fun createDriver(): SqlDriver
}

Why are some of these imports from the same library returning as unresolved references?

So I am currently trying to add a room database to an open-source project as part of my thesis, I grabbed the most recent example I could from android itself and modified it for Kotlin DSL, I used the same DAO and database structure but both have import errors. Normally this wouldn't be a problem to fix except I am getting errors from the same library in places which is why I had to ask this question in the first place. The main import problem is to do with androidx.room. Below is the kotlin file in question and after that, is the build.gradle.kts.
Just to add, android studio suggests I use persistence.room.runtime but from what I have tried and what I could find, this does not solve my problem either.
import androidx.room.Database //works
import androidx.room.Room // unresolved reference
import androidx.room.RoomDatabase // unresolved reference
#Database(entities = [GameDatabase::class], version = 1, exportSchema = false)
abstract class Database : RoomDatabase() {
abstract val DatabaseDAO: DatabaseDAO
companion object {
#Volatile
private var INSTANCE: Database? = null
fun getInstance(): Database {
synchronized(this) {
var instance = INSTANCE
if (instance == null) {
instance = Room.databaseBuilder(
Database::class.java,
"game_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
}
return instance
}
}
}
}
Here is the module gradle build:
plugins {
//id("kotlin")
kotlin("kapt")
//kotlin("android")
//kotlin("android-extensions")
//id("kotlin-android")
//id("kotlin-android-extensions")
//id("androidx.navigation.safeargs")
}
java {
sourceCompatibility = JavaVersion.VERSION_1_6
}
tasks {
compileJava {
options.encoding = "UTF-8"
}
compileTestJava {
options.encoding = "UTF-8"
}
}
sourceSets {
main {
java.srcDir("src/")
}
}
// added
dependencies {
val room_version = "2.2.6"
val life_version = "2.2.0"
// Room and Lifecycle dependencies
implementation("androidx.room:room-runtime:$room_version")
kapt("androidx.room:room-compiler:$room_version")
//implementation("androidx.room:room-ktx:$room_version")
implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("androidx.lifecycle:lifecycle-extensions:$life_version")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$life_version")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$life_version")
implementation("androidx.lifecycle:lifecycle-common-java8:$life_version")
// adding Firebase dependencies
//implementation("android.arch.persistence.room:runtime:1.1.1")
//annotationProcessor("android.arch.persistence.room:compiler:2.2.6")
}
I am quite new to Kotlin DSL so it might be an obvious error but I also couldn't find anything like this in the area.
This is how I usually place the implementations:
//Room
implementation "androidx.room:room-runtime:2.2.6"
kapt "androidx.room:room-compiler:2.2.6"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:2.2.6"
Also, have you placed the kapt plugin? if not:
plugins {
id 'kotlin-android'
id 'kotlin-kapt'//this one
}
And, try to clean and rebuild the project.

Testing Room as JUnit Test Not AndroidTest

Trying to test room migration using the MigrationTestHelper class and Robolectric. We want it as a JUnit test because our CI environment cannot fire up an emulator. (Please no answers with CI fixes for emulators, CI is not in my control) Only issue I have is that the test fails because it can't find the schemas. My build.gradle has this in it already
android {
sourceSets {
test.assets.srcDirs += files("$projectDir/schemas".toString())
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
test {
java.srcDirs += "$projectDir/src/sharedTest/java"
}
testOptions {
unitTests {
includeAndroidResources = true
}
unitTests.all {
systemProperty 'robolectric.enabledSdks', '21'
}
}
}
dependencies {
// has all the proper dependencies from mockito adn robolectric to kotlin and junit.
}
Here is the test code but again its mostly just the schema can't be found when the database creation is called. Also the json files are there in the schema directory
#RunWith(RobolectricTestRunner::class)
class Migration19To20Test {
private val migration = MyDatabase.MIGRATION_19_20
private val fromVersion = 19
private val toVersion = 20
#get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
MyDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory())
private val testDatabaseName = "migration-test"
#Test
fun insertsFirmwareVersionFullColumn() {
givenADatabase()
val validateDroppedTables = true
val db = helper.runMigrationsAndValidate(
testDatabaseName,
toVersion,
validateDroppedTables,
migration)
db.query("select * from ${DatabaseConstants.Table.People}").use { cursor ->
cursor.moveToFirst()
assertTrue("table should contain the ${DatabaseConstants.Column.People.NAME_FULL} column as it should have been added",
cursor.columnNames.contains(DatabaseConstants.Column.People.NAME_FULL))
}
}
private fun givenADatabase() {
// Test fails here
helper.createDatabase(testDatabaseName, fromVersion)
}
}
I solved it by copying MigrationTestHelper to my test source code and modified loadSchema method to look like this
private SchemaBundle loadSchema(Context context, int version) throws IOException {
// InputStream input = context.getAssets().open(mAssetsFolder + "/" + version + ".json");
InputStream input = new FileInputStream("./schemas/" + mAssetsFolder + "/" + version + ".json");
return SchemaBundle.deserialize(input);
}
The schemas directory is configured in build.gradle
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [
"room.schemaLocation" : "$projectDir/schemas".toString(),
"room.incremental" : "true",
"room.expandProjection": "true"]
}
}
}
}
As you can see the cause is that this class looks for migration in assets directory because Google assumes that you will run those migration on device. In such case you must include them as part of android assets.
With Robolectric it's just reading from a directory and putting that into a stream.
My scenario was not exactly the same - I had an instrumented migration test case - but perhaps the cause is the same. If you have something like the rule below, aapt will strip the schema files and it will do so in debug builds as well as release ones.
buildTypes {
debug {...}
release {
aaptOptions {
ignoreAssetsPattern '!*.json'
}
}
}

How to override the Robolectric runtime dependency repository URL?

We're trying to use the org.robolectric:robolectric:3.0 dependency from our own internal Nexus repository. The issue is that Robolectric tries to load some dependencies at runtime from a public repository (as mentioned here), and ignores any repository overrides in the build.gradle.
Since we don't have access to that public location from our intranet, my tests timeout after trying to load that dependency:
[WARNING] Unable to get resource
'org.robolectric:android-all:jar:5.0.0_r2-robolectric-1' from
repository sonatype (https://oss.sonatype.org/content/groups/public/):
Error transferring file: Operation timed out
The bottom section of the Robolectric configuration documentation recommends adding this to your Gradle configuration to override the URL:
android {
testOptions {
unitTests.all {
systemProperty 'robolectric.dependency.repo.url', 'https://local-mirror/repo'
systemProperty 'robolectric.dependency.repo.id', 'local'
}
}
}
Unfortunately, I've tested that and I never see that system property being set. I've printed it out from inside my custom Robolectric runner (which extends RobolectricGradleTestRunner) and that system property remains set to null.
System.out.println("robolectric.dependency.repo.url: " + System.getProperty("robolectric.dependency.repo.url"));
I also tried to do something similar to this comment (but that method doesn't exist to override in RobolectricGradleTestRunner), and I also tried setting the system properties directly in my custom Robolectric runner, and that didn't seem to help.
#Config(constants = BuildConfig.class)
public class CustomRobolectricRunner extends RobolectricGradleTestRunner {
private static final String BUILD_OUTPUT = "build/intermediates";
public CustomRobolectricRunner(Class<?> testClass) throws InitializationError {
super(testClass);
System.setProperty("robolectric.dependency.repo.url", "https://nexus.myinternaldomain.com/content");
System.setProperty("robolectric.dependency.repo.id", "internal");
System.out.println("robolectric.dependency.repo.url: " + System.getProperty("robolectric.dependency.repo.url"));
}
The Robolectric source code does seem to confirm that these system properties exist.
While not a fix for using the properties directly, another way to get this to work is by overriding getJarResolver() in a RobolectricTestRunner subclass and pointing it at your artifact host:
public final class MyTestRunner extends RobolectricTestRunner {
public MyTestRunner(Class<?> testClass) throws InitializationError {
super(testClass);
}
#Override protected DependencyResolver getJarResolver() {
return new CustomDependencyResolver();
}
static final class CustomDependencyResolver implements DependencyResolver {
private final Project project = new Project();
#Override public URL[] getLocalArtifactUrls(DependencyJar... dependencies) {
DependenciesTask dependenciesTask = new DependenciesTask();
RemoteRepository repository = new RemoteRepository();
repository.setUrl("https://my-nexus.example.com/content/groups/public");
repository.setId("my-nexus");
dependenciesTask.addConfiguredRemoteRepository(repository);
dependenciesTask.setProject(project);
for (DependencyJar dependencyJar : dependencies) {
Dependency dependency = new Dependency();
dependency.setArtifactId(dependencyJar.getArtifactId());
dependency.setGroupId(dependencyJar.getGroupId());
dependency.setType(dependencyJar.getType());
dependency.setVersion(dependencyJar.getVersion());
if (dependencyJar.getClassifier() != null) {
dependency.setClassifier(dependencyJar.getClassifier());
}
dependenciesTask.addDependency(dependency);
}
dependenciesTask.execute();
#SuppressWarnings("unchecked")
Hashtable<String, String> artifacts = project.getProperties();
URL[] urls = new URL[dependencies.length];
for (int i = 0; i < urls.length; i++) {
try {
urls[i] = Util.url(artifacts.get(key(dependencies[i])));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
return urls;
}
#Override public URL getLocalArtifactUrl(DependencyJar dependency) {
URL[] urls = getLocalArtifactUrls(dependency);
if (urls.length > 0) {
return urls[0];
}
return null;
}
private String key(DependencyJar dependency) {
String key =
dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getType();
if (dependency.getClassifier() != null) {
key += ":" + dependency.getClassifier();
}
return key;
}
}
}
It should be noted that this relies on two internal classes of Robolectric so care should be taken when upgrading versions.
You can set properties mavenRepositoryId and mavenRepositoryUrl of RoboSettings which are used by MavenDependencyResolver.
Example:
public class CustomRobolectricRunner extends RobolectricGradleTestRunner {
static {
RoboSettings.setMavenRepositoryId("my-nexus");
RoboSettings.setMavenRepositoryUrl("https://my-nexus.example.com/content/groups/public");
}
...
}
As per the linked Github issue, one fix is to configure a settings.xml in your ~\.m2 folder:
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<mirrors>
<mirror>
<id>jcenter</id>
<name>JCenter Remote</name>
<mirrorOf>*</mirrorOf>
<url>https://www.example.com/artifactory/jcenter-remote/</url>
</mirror>
</mirrors>
</settings>
<mirrorOf>*</mirrorOf> seems necessary to force Maven to redirect all repository requests to the one remote. See here for more details about mirror settings in Maven.
I found that using a remote of Sonatype is not sufficient, you should use a remote of JCenter or Maven Central in order to obtain all of the transitive dependencies.
As of time of this writing, those previous answers are now obsolete. If you refer to the latest robolectric documentation you need to override the robolectric.dependency.repo.url property like so:
android {
testOptions {
unitTests.all {
systemProperty 'robolectric.dependency.repo.url', 'https://local-mirror/repo'
systemProperty 'robolectric.dependency.repo.id', 'local'
}
}
}

How to run 2 queries sequentially in a Android RxJava Observable?

I want to run 2 asynchronous tasks, one followed by the other (sequentially). I have read something about ZIP or Flat, but I didn't understand it very well...
My purpose is to load the data from a Local SQLite, and when it finishes, it calls the query to the server (remote).
Can someone suggests me, a way to achieve that?
This is the RxJava Observable skeleton that I am using (single task):
// RxJava Observable
Observable.OnSubscribe<Object> onSubscribe = subscriber -> {
try {
// Do the query or long task...
subscriber.onNext(object);
subscriber.onCompleted();
} catch (Exception e) {
subscriber.onError(e);
}
};
// RxJava Observer
Subscriber<Object> subscriber = new Subscriber<Object>() {
#Override
public void onCompleted() {
// Handle the completion
}
#Override
public void onError(Throwable e) {
// Handle the error
}
#Override
public void onNext(Object result) {
// Handle the result
}
};
Observable.create(onSubscribe)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber);
The operator to do that would be merge, see http://reactivex.io/documentation/operators/merge.html.
My approach would be to create two observables, let's say observableLocal and observableRemote, and merge the output:
Observable<Object> observableLocal = Observable.create(...)
Observable<Object> observableRemote = Observable.create(...)
Observable.merge(observableLocal, observableRemote)
.subscribe(subscriber)
If you want to make sure that remote is run after local, you can use concat.
Lukas Batteau's answer is best if the queries are not dependent on one another. However, if it is necessary for you obtain the data from the local SQLite query before you run the remote query (for example you need the data for the remote query params or headers) then you can start with the local observable and then flatmap it to combine the two observables after you obtain the data from the local query:
Observable<Object> localObservable = Observable.create(...)
localObservable.flatMap(object ->
{
return Observable.zip(Observable.just(object), *create remote observable here*,
(localObservable, remoteObservable) ->
{
*combining function*
});
}).subscribe(subscriber);
The flatmap function allows you to transform the local observable into a combination of the local & remote observables via the zip function. And to reiterate, the advantage here is that the two observables are sequential, and the zip function will only run after both dependent observables run.
Furthermore, the zip function will allow you to combine observables even if the underlying objects have different types. In that case, you provide a combining function as the 3rd parameter. If the underlying data is the same type, replace the zip function with a merge.
You can try my solutions, there are several ways to resolve your problem.
To make sure it's working, I created a stand alone working example and use this API to test: https://jsonplaceholder.typicode.com/posts/1
private final Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/posts/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
private final RestPostsService restPostsService = retrofit.create(RestPostsService.class);
private Observable<Posts> getPostById(int id) {
return restPostsService.getPostsById(id);
}
RestPostService.java
package app.com.rxretrofit;
import retrofit2.http.GET;
import retrofit2.http.Path;
import rx.Observable;
/**
* -> Created by Think-Twice-Code-Once on 11/26/2017.
*/
public interface RestPostsService {
#GET("{id}")
Observable<Posts> getPostsById(#Path("id") int id);
}
Solution1: Use when call multiple tasks in sequences, the result of previous tasks is always the input of the next task
getPostById(1)
.concatMap(posts1 -> {
//get post 1 success
return getPostById(posts1.getId() + 1);
})
.concatMap(posts2 -> {
//get post 2 success
return getPostById(posts2.getId() + 1);
})
.concatMap(posts3 -> {
//get post 3success
return getPostById(posts3.getId() + 1);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(finalPosts -> {
//get post 4 success
Toast.makeText(this, "Final result: " + finalPosts.getId() + " - " + finalPosts.getTitle(),
Toast.LENGTH_LONG).show();
});
Solution2: Use when call multiple tasks in sequences, all results of previous tasks is the input of the final task (for example: after uploading avatar image and cover image, call api to create new user with these image URLs):
Observable
.zip(getPostById(1), getPostById(2), getPostById(3), (posts1, posts2, posts3) -> {
//this method defines how to zip all separate results into one
return posts1.getId() + posts2.getId() + posts3.getId();
})
.flatMap(finalPostId -> {
//after get all first three posts, get the final posts,
// the final posts-id is sum of these posts-id
return getPostById(finalPostId);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(finalPosts -> {
Toast.makeText(this, "Final posts: " + finalPosts.getId() + " - " + finalPosts.getTitle(),
Toast.LENGTH_SHORT).show();
});
AndroidManifest
<uses-permission android:name="android.permission.INTERNET"/>
root build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
classpath 'me.tatarka:gradle-retrolambda:3.2.0'
classpath 'me.tatarka.retrolambda.projectlombok:lombok.ast:0.2.3.a2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
// Exclude the version that the android plugin depends on.
configurations.classpath.exclude group: 'com.android.tools.external.lombok'
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
app/build.gradle
apply plugin: 'me.tatarka.retrolambda'
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
buildToolsVersion "26.0.1"
defaultConfig {
applicationId "app.com.rxretrofit"
minSdkVersion 15
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:26.+'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
provided 'org.projectlombok:lombok:1.16.6'
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
compile 'com.squareup.retrofit2:adapter-rxjava:2.3.0'
compile 'io.reactivex:rxandroid:1.2.1'
}
model
package app.com.rxretrofit;
import com.google.gson.annotations.SerializedName;
/**
* -> Created by Think-Twice-Code-Once on 11/26/2017.
*/
public class Posts {
#SerializedName("userId")
private int userId;
#SerializedName("id")
private int id;
#SerializedName("title")
private String title;
#SerializedName("body")
private String body;
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
}
By the way, use Rx + Retrofit + Dagger + MVP pattern is a great combine.

Categories

Resources