Developer Log - Plastix/Forage GitHub Wiki
This is the development log of Forage. I'll be writing about my learning process in creating my first Android application. Don't expect this to be very high quality writing.
CircleCI is a continuous integration build service. I've been trying to set up my Android project on it however I cannot get my build passing.
I'm having trouble mocking locations for my functional tests through GoogleApiClient. I keep getting security warnings although I have granted mock location permissions.
I'm having issues setting the path for a custom instrumentation runner on Android.
Turns out mocking a webserver is a little harder than expected. I need to reconfigure my Retrofit service's base URL to point to a mocked webserver URL. This has to be done with an OkHttp Interceptor (https://github.com/square/okhttp/wiki/Interceptors). However, I'm currently having issues getting my network service to connect to the mockwebserver correctly.
Turns out the issue was one of my gradle plugins, AndroidDevMetrics. For some reason these two dependencies don't get along with each other and break with absolutely no errors or warnings. It took nearly 2 weeks to track this issue down.
I learned that the lag on the compass screen was due to RxJava backpressure issues. This meant that my observables were emitting data faster than the subscriber could handle it (In this case faster than the view could animate and move the compass). I solved this issue by utilizing some of RxJava's backpressure operators (Observable.sample()
) to "throttle" the data being emitted from my observable. Now the compass screen works well even on a 4 year old device such as the Samsung S3!
My app crashes on a Samsung s3 with the most cryptic error ever. I'm thinking this is an Android build tools issue?
03-31 16:03:01.473 6519-6519/io.github.plastix.forage E/AndroidRuntime: FATAL EXCEPTION: main
Process: io.github.plastix.forage, PID: 6519
java.lang.VerifyError: io/github/plastix/forage/ui/cachelist/CacheListActivity$AjcClosure1
at io.github.plastix.forage.ui.cachelist.CacheListActivity.onCreate(CacheListActivity.java:46)
at android.app.Activity.performCreate(Activity.java:5451)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1093)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2298)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2392)
at android.app.ActivityThread.access$900(ActivityThread.java:169)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1280)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:146)
at android.app.ActivityThread.main(ActivityThread.java:5487)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1283)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1099)
at dalvik.system.NativeStart.main(Native Method)
I'm having issues running Roboelectric tests because they hang indefinitely. Anything I do does't fix this problem. 😢
What I've learned from the past few days of refactoring is that retained fragments were not a very good decision. They are the "easy way" to solve configuration changes but are costly in the long run. The new MVP system does not retain fragments or presenters. Instead, it retains the observable that is used in the presenter outside of the Activity lifecycle using a Singleton provided by Dagger 2. I thought about retaining the presenter but it is hard to know when that itself needs to be "released".
I found out today that I was doing Dagger 2 scopes all wrong... You can't just throw @Singleton
on everything! I needed some custom scopes. Going to do a refactor of my dependency injection code this week.
I'm having a hard time getting my compass arrow to animate nicely. It easier wants to "snap to" the angle or it won't animate properly. For some reason when the angle changes from 0 to 360 it likes to animate all the way around the circle instead of taking the short delta.
Apparenrently Android rotates views counter clockwise? Hence I have to rotate my compass arrow -degrees
!
The Android compass sensor is extremely complicated. I've seen some examples like this one that use the magnetic sensor and accelerometer to calculate the azimuth of the compass. Because these are really low level APIs it is hard to tell what is going on:
private int mAzimuth = 0; // degree
private SensorManager mSensorManager = null;
private Sensor mAccelerometer;
private Sensor mMagnetometer;
boolean haveAccelerometer = false;
boolean haveMagnetometer = false;
private SensorEventListener mSensorEventListener = new SensorEventListener() {
float[] gData = new float[3]; // accelerometer
float[] mData = new float[3]; // magnetometer
float[] rMat = new float[9];
float[] iMat = new float[9];
float[] orientation = new float[3];
public void onAccuracyChanged( Sensor sensor, int accuracy ) {}
@Override
public void onSensorChanged( SensorEvent event ) {
float[] data;
switch ( event.sensor.getType() ) {
case Sensor.TYPE_ACCELEROMETER:
gData = event.values.clone();
break;
case Sensor.TYPE_MAGNETIC_FIELD:
mData = event.values.clone();
break;
default: return;
}
if ( SensorManager.getRotationMatrix( rMat, iMat, gData, mData ) ) {
mAzimuth= (int) ( Math.toDegrees( SensorManager.getOrientation( rMat, orientation )[0] ) + 360 ) % 360;
}
}
}
**
@Override
protected void onCreate( Bundle savedInstanceState ) {
this.mAccelerometer = this.mSensorManager.getDefaultSensor( Sensor.TYPE_ACCELEROMETER );
this.haveAccelerometer = this.mSensorManager.registerListener( mSensorEventListener, this.mAccelerometer, SensorManager.SENSOR_DELAY_GAME );
this.mMagnetometer = this.mSensorManager.getDefaultSensor( Sensor.TYPE_MAGNETIC_FIELD );
this.haveMagnetometer = this.mSensorManager.registerListener( mSensorEventListener, this.mMagnetometer, SensorManager.SENSOR_DELAY_GAME );
if ( haveAccelerometer && haveMagnetometer ) {
// ready to go
} else {
// unregister and stop
}
}
Solution: I've decided to go with the "fused" compass sensor which is a software sensor that pulls from the accelerometer, magnetometer, and gyroscope if available.
Implementing Android UI is a really painful process. Finding the right XML tags to make your UI exactly how you want it is really hard.
When I started using MVP I was worried that my presenters would leak my view. However, since I'm exclusively using retained fragments as my views, it doesn't really matter that my presenters have a direct reference to the view since it is retained by the Android system anyway. By using LeakCanary, I found out that it is really easy to accidentally leak an activity. My CacheListActivity
was leaking because the CacheAdapter
in my CacheListFragment
still had a reference to it. When I set the adapter to null when the view was destroyed it fixed my leak.
Looks like I'm dropping some frames on my new Cache detail view.... According to the Android monitor the issue is Expensive measure/layout pass.
I need some custom serialization for my geocaches so I've opted to switch back to Retrofit's Gson adapters. However, OkApi returns one big json object of caches instead of a json array. I'm having a hard time getting gson to use a custom deserializer.
Solution: I managed to get it working by having a custom List type adapter. Since Gson needs to deserialize the generic type of the list I have to use some internal Gson APIs in order to delegate the generic type deserialization.
Solution 2: The better solution is to filter the observable of loaded objects so it never emits any that are not loaded.
realm.where(Cache.class).contains("code", cacheCode).findFirstAsync()
// Must filter by loaded objects because findFirstAsync returns a "Future"
.<Cache>asObservable().filter(new Func1<Cache, Boolean>() {
@Override
public Boolean call(Cache cache) {
return cache.isLoaded();
}
})
.take(1).toSingle()
.observeOn(AndroidSchedulers.mainThread());
Getting an error that my Realm object isn't loaded when accessing it from my observable subscriber.
realm.where(Cache.class).contains("code", cacheCode).findFirstAsync()
.<Cache>asObservable()
Solution: Turns out this issue is based on my fundamental misunderstanding of findFirstAsync and observables. Since I'm using a rx.Single, Realm emits an unloaded RealmObject to the single. Since the single never emits another item, there is no way to know when the RealmObject is loaded. So I decided to use a non-async call.
I always have a tendency to forget to register things in my AndroidManifest.xml
. For example I forgot to give my CacheDetailActivity
a theme in my manifest.
<activity android:name=".ui.cachedetail.CacheDetailActivity"
android:theme="@style/AppTheme.NoActionBar">
</activity>
vs
<activity android:name=".ui.cachedetail.CacheDetailActivity"/>
<!-- Errors!!! -->
Like many of Google's APIs, Runtime permissions on Android M are pretty complicated. Permission dialog flow is almost as bad as the fragment lolcycle. After much frustration I gave up on creating a "shadow activity" to wrap the permission result in and just use the callback in my activities.
I don't think that anonymous class listeners are working with Realm. There has been some shady issues in the past. See https://github.com/realm/realm-java/issues/1676 and https://github.com/realm/realm-java/issues/1149.
Solution: Don't use an anonymous class listener!
As much as I love RxJava and their Observable paradigm, Observables are a PITA to debug. I think the issue I ran into was the observable.toSingle()
implementation. For some reason it requires that the observable emit two pieces of data. Since my Rx wrapper around the Google Play Location API only emitted one location update, I don't think it was nicely converting my observable to a Single.
Solution: Switched back to singles. Observable.take(1).toSingle()
fixed my Single problem.
I'm trying to write a Rx wrapper around Google Play Location Services. I'm testing on an android emulator and I can't tell if my code isn't working or the emulator isn't working. Location doesn't seem to be an easy thing to get working with an emulator.
I'm getting a android.os.NetworkOnMainThreadException
when using Retrofit with RxJava even though I am chaining my observables like:
api.doNetworkCall() // Returns observable
.observeOn(Schedulers.io())
.subscribeOn(AndroidSchedulers.mainThread());
Solution:
I had the names backwards! observeOn
and subscribeOn
are very similar
api.doNetworkCall()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread());
Getting a NPE from a view that should be injected by Butterknife...
Solution:
I totally missed the documentation for non-activity binding with Butterknife (http://jakewharton.github.io/butterknife/#non-activity). I forgot to pass in a reference to the current fragment object. ButterKnife.bind(this, view)
vs ButterKnife.bind(view)
I started off by implementing my API wrapper and API use case. At first I followed Fernando Cejas' interactor design. However, my API interactor needs a parameter to get nearby caches. I have to rethink my use case architecture. Class inheritance leads to class cohesion which I don't really like.
Solution: I decided to get rid of all of the use case inheritance. Each interactor exposes an observable. It is now the presenter's job to subscribe to it how it sees fit.
For some reason Dagger 2 is not injecting my presenter into the fragment and I get this NPE.
Process: io.github.plastix.forage, PID: 24559
java.lang.NullPointerException: Attempt to invoke virtual method 'void io.github.plastix.forage.ui.cachelist.CacheListPresenter.getStuff()' on a null object reference at io.github.plastix.forage.ui.cachelist.CacheListFragment.onOptionsItemSelected(CacheListFragment.java:72)
Solution:
Turns out one of the limitations of Dagger 2 is that it cannot inject into a super type of an object. See
http://stackoverflow.com/questions/29312911/can-i-just-inject-super-class-when-use-dagger2-for-dependency-injection and
http://stackoverflow.com/questions/29312911/can-i-just-inject-super-class-when-use-dagger2-for-dependency-injection
I had to change the injection method in my CacheListComponent
from void inject(CacheListView view)
to void inject(CacheListFragment fragment)
.