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.
Related
A simplified child module build.gradle.kts:
plugins {
id("com.android.library")
kotlin("android")
}
android {
androidComponents.beforeVariants { it: com.android.build.api.variant.LibraryVariantBuilder ->
it.enabled = run {
// logic to calculate if
it.productFlavors[0].second == "flavor" && it.buildType == "debug"
}
}
}
Is it possible to extract function for calculation of enabled state of buildVariant?
fun calculateIsEnabled(lvb: com.android.build.api.variant.LibraryVariantBuilder): Boolean {
return lvb.productFlavors[0].second == "flavor" && lvb.buildType == "debug"
}
I tried to declare the function in the root build.gradle.kts but I don't know how to access it from submodule and if it is possible at all
I tried to declare it in buildSrc module, but com.android.build.api.variant.LibraryVariantBuilder is undefined here because the plugin com.android.library is not present here and I think it is not allowed and/or meaningless
So, the question is: where to declare a shared function that uses types defined in a gradle plugin and need to be accessible in all submodules of type android library?
After several tries I solved it:
buildSrc/build.gradle.kts
repositories {
google()
mavenCentral()
}
plugins {
`kotlin-dsl`
}
dependencies {
// important: dependency only in simple string format!
implementation("com.android.tools.build:gradle:7.2.0-alpha03")
}
buildSrc/src/main/kotlin/Flavors.kt
import com.android.build.api.variant.LibraryVariantBuilder
import com.android.build.api.variant.ApplicationVariantBuilder
private fun isFlavorEnabled(flavor1: String, buildType: String): Boolean {
return flavor1 == "flavor" && buildType == "debug"
}
fun isFlavorEnabled(lvb: LibraryVariantBuilder): Boolean {
// productFlavors are pairs of flavorType(dimension) - flavorName(selectedFlavor)
return lvb.run { isFlavorEnabled(productFlavors[0].second, buildType ?: "") }
}
fun isFlavorEnabled(avb: ApplicationVariantBuilder): Boolean {
return avb.run { isFlavorEnabled(productFlavors[0].second, buildType ?: "") }
}
In library/build.gradle.kts and app/build.gradle.kts
android {
androidComponents.beforeVariants {
it.enabled = isFlavorEnabled(it)
}
}
I am developing an Android app with Clean Architecture.
So I separated the modules to 'app', 'data', 'domain'.
But what I want to do is using an android.utils.Log in the module 'data', 'domain'.
I cannot find it in the 'data', 'domain' modules.
Below is my 'domain' module's gradle file.
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
sourceCompatibility = "7"
targetCompatibility = "7"
Should I add something in here?
Or should I just use "System.out.println()" method?
First of all I have to confirm your intentions are good :)
You need to do the following to achieve what you want:
Define final Log class + interface defining log, both in your domain:
public final class Log {
private static LogInterface logInterface;
public static void d(String tag, String message) {
logInterface.d(tag, message);
}
public static void setLogInterface(LogInterface logInterface) {
Log.logInterface = logInterface;
}
public interface LogInterface {
void d(String tag, String message);
//...
}
}
Notice all of the above are pure java, nothing bound to android.
In any of your android modules, create and implement android logger:
public class AndroidLog implements Log.LogInterface {
public void d(String tag, String message) {
android.util.Log.d(tag, message);
}
}
Probably in the same module as p2, initialize (initialization likely should happen when app is created) :
Log.setLogInterface(new AndroidLogger());
Now you can use your domain's Log like this: Log.d(...) - all around in your pure java modules.
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.
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'
}
}
}
I am creating an Android app using Google App Engine. In order to use GCM (Google Cloud Messaging), I have created a GCM module in Android Studio. This module provides a sample code that registers devices in the Datastore.
All was working well yesterday, and although nothing changed, I have this error when I try to register my device :
java.lang.NoSuchMethodError: com.google.appengine.api.datastore.Cursor: method <init>()V not found
I don't know what exactly means the notation <init>()V, but I found the Cursor class to be generated by the Google plugin of Android Studio, here :
This is the decompiled code inside Cursor.class :
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.google.appengine.api.datastore;
import java.io.Serializable;
public final class Cursor implements Serializable {
private String webString;
public Cursor(String webString) {
this.webString = webString;
}
public String toWebSafeString() {
return this.webString;
}
public static Cursor fromWebSafeString(String encodedCursor) {
if(encodedCursor == null) {
throw new NullPointerException("encodedCursor must not be null");
} else {
return new Cursor(encodedCursor);
}
}
public boolean equals(Object o) {
if(this == o) {
return true;
} else if(o != null && this.getClass() == o.getClass()) {
Cursor cursor = (Cursor)o;
return this.webString.equals(cursor.webString);
} else {
return false;
}
}
public int hashCode() {
return this.webString.hashCode();
}
public String toString() {
return this.webString;
}
}
Finally, this is my build.gradle :
// If you would like more information on the gradle-appengine-plugin please refer to the github page
// https://github.com/GoogleCloudPlatform/gradle-appengine-plugin
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.google.appengine:gradle-appengine-plugin:1.9.18'
}
}
repositories {
jcenter();
}
apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'appengine'
sourceCompatibility = 1.7
targetCompatibility = 1.7
dependencies {
appengineSdk 'com.google.appengine:appengine-java-sdk:1.9.18'
compile 'com.google.appengine:appengine-endpoints:1.9.18'
compile 'com.google.appengine:appengine-endpoints-deps:1.9.18'
compile 'javax.servlet:servlet-api:2.5'
compile 'com.googlecode.objectify:objectify:4.0b3'
compile 'com.ganyo:gcm-server:1.0.2'
}
appengine {
downloadSdk = true
appcfg {
oauth2 = true
}
endpoints {
getClientLibsOnBuild = true
getDiscoveryDocsOnBuild = true
}
}
Because I changed nothing in the concerned code, I really can't understand what happened and I found nothing useful on the web.
Thank you in advance for your help.
Edit : StackTrace from the backend log
It's looking for the empty constructor : method init()v not found
It appears this could be because Cursor.java is pulled from your <module>/build/classes/main (or <module>/build/exploded-app/WEB-INF/classes/main), when really it should just be pulled in from a library appengine-api-1.0-sdk-<version>.jar.
Have you added the source for a Cursor.java into your project src folder somehow? The App Engine build creates a runnable build at <module>/build/exploded-app and the cursor class is usually sourced from <module>/build/exploded-app/WEB-INF/lib/appengine-api-1.0-sdk-<version>.jar