Droidcon 2015 London (Part 3)

Liudas Survila
10 min readNov 9, 2015

--

More on Day 2 talks of Droidcon. Part1. Part2.

Talks covered in this part:

  • Staying alive, online and offline
  • Working Together: Avoiding house divided with developers and designers
  • Understanding scrolling techniques in Android

Staying alive, online and offline

by @ErikHellman

Was very interested in this topic. I always though that most of the apps don’t deal much with edge cases of connectivity. We usually live in big cities, where network coverage is very good and in the worst case we are used to good networks, but that’s not always the case. Mentioned article of Syrian refugees using their smartphones. Also, check out my friend’s Chris thoughts on offline apps. Slides of the talk here.

Why should we make our apps offline:

  • reduce glitches
  • reduce data costs (also important for roaming users)
  • missing coverage
  • improve performance (loading data from cache)

Ways to improve:

  • Detect connectivity and handle it

Register BroadcastReceiver (will continuously listen for network state changes). Remember to unregister when fragment or activity using it is destroyed.

private void setupNetworkChangeListener() {  
IntentFilter intentFilter = new
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
networkStateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
ConnectivityManager manager = (ConnectivityManager)
getSystemService(CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = manager.getActiveNetworkInfo();
// handle network state here
notifyNetworkState(networkInfo != null &&
networkInfo.isConnected());
}
};
registerReceiver(networkStateReceiver, intentFilter);
}

Returned NetworkInfo has an enum NetworkInfo.State, usually it’s enough to determine if a user is connected or not, but if you want to provide more information to a user about his network, there is NetworkInfo.DetailedState. This is how these states maps:

private static final EnumMap<DetailedState, State> stateMap = 
new EnumMap<DetailedState, State>(DetailedState.class);

static {
stateMap.put(DetailedState.IDLE, State.DISCONNECTED);
stateMap.put(DetailedState.SCANNING, State.DISCONNECTED);
stateMap.put(DetailedState.CONNECTING, State.CONNECTING);
stateMap.put(DetailedState.AUTHENTICATING, State.CONNECTING);
stateMap.put(DetailedState.OBTAINING_IPADDR, State.CONNECTING);
stateMap.put(DetailedState.VERIFYING_POOR_LINK, State.CONNECTING);
stateMap.put(DetailedState.CAPTIVE_PORTAL_CHECK,
State.CONNECTING);
stateMap.put(DetailedState.CONNECTED, State.CONNECTED);
stateMap.put(DetailedState.SUSPENDED, State.SUSPENDED);
stateMap.put(DetailedState.DISCONNECTING, State.DISCONNECTING);
stateMap.put(DetailedState.DISCONNECTED, State.DISCONNECTED);
stateMap.put(DetailedState.FAILED, State.DISCONNECTED);
stateMap.put(DetailedState.BLOCKED, State.DISCONNECTED);
}

Being on 2G gives its own difficulties. If you are making a phone call when you are on 2G, there is no network (State.DISCONNECTED)

To detect if user is on a Flight Mode:

public static boolean isAirplaneModeOn(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
return Settings.System.getInt(context.getContentResolver(),
Settings.System.AIRPLANE_MODE_ON, 0) != 0;
} else {
return Settings.Global.getInt(context.getContentResolver(),
Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
}
}
  • Set reasonable timeout

A network can be very slow, but still working, how long should we wait?

Short, too aggressive timeout example — Spotify showing ‘You’re offline’ on startup even if you are on 4G. Article mentioned by Jakob Nielsen says:

…response time guidelines for web-based applications are the same as for all other applications…

0.1s — system is reacting instantaneously

1.0s — limit for user’s flow of thought to stay uninterrupted

10s — limit for keeping the users’ attention

To set timeout:

// for default HttpURLConnection
URL url = new URL(urlToConnect);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setConnectTimeout(CONNECTION_TIMEOUT);
urlConnection.setReadTimeout(CONNECTION_TIMEOUT);

// if you are using OkHttp, and you should (solves some problems
// itself)
OkHttpClient client = new OkHttpClient();
client.setConnectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS);
client.setReadTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS);

// do not use apache http client, it is deprecated and even removed on Android 6
  • Request queuing and serialisation

You can queue network requests by using IntentService.

// In your Activity
startService(buildRequestIntent(url, data));

// IntentService.onHandleIntent()
if (isConnected(this)) {
performRequest(url, data);
} else {
serializeRequest(url, data);
}

// BroadcastReceiver listening for network changes
// Disable it when there is no queued requests left.
if (isConnected(context)) {
context.startService(new Intent(ACTION_SEND_QUEUED_REQUESTS));
}
private serializeRequest(String url, String data) {
// One of the most simplest ways is to serialize with
// ContentValues and use it in Sqlite database (through
// ContentResolver)
ContentValues values = new ContentValues();
values.put("url", url);
values.put("data", data);
// we can also put headers if they have any meaning, have
//timestamps, flags, etc...
getContentResolver.insert(...);
}

Note from me, if you don’t want to deal with network queuing by yourself, there is a very popular library Retrofit, which also is using OkHttp and is highly compatible with RxJava (also check out Retrofit2 presentation!). Also, Android has its own network queueing library called Volley. Mentioned alternatives Firebase and Couchebase, but those will require changes on a server.

  • Cache data

You data should be cached (will save network costs and improve performance). A cache can be stored in Context.getCacheDir(), Context.getFilesDir() or Context.getExternalFilesDir. If you have a response with expired age and you are offline, don’t evict that data! (it’s most of the time still better than nothing). If you don’t want to implement caching mechanism yourself, you can use LruCache and DiskLruCache.

If you are using RxJava, you can chain your caching logic like in this blog post by Dan Lew:

// Data sources
Observable<Data> memory = ...;
Observable<Data> disk = ...;
Observable<Data> network = ...;
// Retrieve the first source with data
Observable<Data> source = Observable
.concat(memory, disk, network)
.first();

Erik also mentioned that using HTTP response cache requires a network connection. However you can still use it offline if you are using OkHttp like this:

OkHttpClient okHttpClient = new OkHttpClient();
File httpCacheDirectory = new File(context.getCacheDir(),
CACHE_DIRECTORY);
Cache cache = new Cache(httpCacheDirectory, CACHE_SIZE);
okHttpClient.setCache(cache);
// You can intercept OkHttp client and manually set response
// headers (but it's not recommended practice!)
okHttpClient.networkInterceptors.add(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
return response.newBuilder()
.header("Cache-Control", String.format("max-age=%d, public",
CACHE_MAX_AGE))
.build;
}
});
  • Preload data to APK

Sometimes it makes sense to preload data into APK, so a user will have something while data is downloaded. You can use raw and assets directory (max APK size = 100mb). If you require more space, you can use APK Expansion Files or you can download data at first startup (however this can be unexpected to a user and lead to bad UX, in this case, a proper description might help). Also to note that small apps are more likely to be downloaded from Play Store and you need to consider different demographics, where download speed can be very slow.

Example, preloading SQLite Database from raw folder:

private SQLiteDatabase openPreloadedSQLiteDB() { 
File localCopy = new File(getFilesDir(), DB_NAME);
if (localCopy.exists()) {
// TODO Perform version check!
InputStream inputStream =
getResources().openRawResource(R.raw.preloaded_db);
readStreamToFile(inputStream, localCopy);
}
return SQLiteDatabase.openOrCreateDatabase(localCopy, null);
}

To make an SQLite database, that can be used later to preload, use Android’s SQLite implementation (as other might have inconsistencies).

Some devices can have very low internal storage, to use an external for either caching or preloading (also Android M can adopt SDCard as internal storage):

public static void saveFileToExternalStorage(Context context, String 
filename, byte[] data) {
File appPrivateFile = new File(context.getExternalFilesDir(null),
filename);
writeBytesToFile(data, appPrivateFile);
}

For areas where there is no coverage at all. Maybe this could be a solution (using Bluetooth or WiFi Direct). For future discussions.

#sketchnotes by @corey_latislaw

Working Together: Avoiding house divided with developers and designers

by @lehtimaeki

A great talk how to make developers and designers talk to each other.

What we want to avoid:

  • Building bad apps from great design
  • Bad design ruining good development effort

Is this design spec final? I will not be changing the UI! — developer

Programming is not difficult, I’ve done HTML, I know — designer

My daily driver phone is an iPhone — designer

Oh, come on! I already changed the code once! — developer

Yeah, we have a designer. He’ll make the icons — project manager

Can’t be done! — developer

How to avoid:

  • No design spec is perfect. Iteration. After the first iteration, the first APK is going to be spec. And from there on work with designers back and forth to improve it.
  • Prioritise easy to implement and important for UX tasks

Designers must:

  • Use Android as a daily phone. You can’t properly design for something you don’t know how it works.
  • Read the guidelines!
  • Don’t spec something that is already provided by the platform. (like dps on Toolbar, etc).
  • Don’t cut assets that can be easily implemented by code (shapes, vectors, etc)
  • Don’t spec translucency overlays, it’s expensive on performance. Avoid if possible.
  • Use same file naming, format conventions as developers.
  • Use ROBOTO font, it’s well-tested font on various screen sizes and densities by Google.
  • Take time understanding how Android works (for example how different phone sizes and densities scale).

Developers must:

  • Don’t ignore design details
  • Read the guidelines!
  • Be on the bleeding edge, follow communities, use latest Android tech.
  • Read the Android Design Support Lib docs (don’t implement stuff that Google has already implemented)
  • Search Google, Stackoverflow, Github for solutions (if you can’t find any official implementations)
  • If you can’t find a solution, take a coffee break, think and propose an alternative.
  • Don’t make a judgement on a design from your perspective, the app is not for you. Talk with designers, ask about things you don’t understand.
  • Don’t over engineer for simple things.
  • Take time explaining how Android works (for example how different phone sizes and densities scale).

Understanding scrolling techniques in Android

by @cyrilmottier

Check out the slides.

Scrolling (on Android) is complicated.

One of the biggest problems on Android is that there are lots of scrolling containers:

Listening to scrolling

// You must override ScrollView
public class YoScrollView extends ScrollView {
// Called when scroll properties changed.
// Gives old and new scroll values.
@Override protected void onScrollChanged(int l, int t, int oldl,
int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
}

}
// API 23+ you can set scroll listener to any view
findViewById(android.R.id.scrollView)
.setOnScrollChangeListener(new OnScrollChangeListener() {

@Override public void onScrollChange(View v, int scrollX, int
scrollY, int oldScrollX, int oldScrollY) {
}

});
// You can listen at view hierarchy, no parameters
getWindow().getDecorView().getViewTreeObserver()
.addOnScrollChangedListener(new
ViewTreeObserver.OnScrollChangedListener() {
@Override
public void onScrollChanged() {
// A View in the hierarchy scrolled
}
});

ScrollView and WebView will provide values relative to screen size. ListView and similar views will provide values only if over-scrolled relative to item size. A view like DrawerLayout and MapView won’t provide any values.

Steps to develop a perfect scrolling container:

  1. Initialisation. Extend ViewGroup.
// Inform the system the View is scrollable container
// System will resize view appropriately (when keyboard is up), etc
setScrollContainer(true)
@Override
// Prevents the pressed state from appearing when the user is
// actually trying to scroll content. (should be true by default)
public boolean shouldDelayChildPressedState() {
return true;
}
// Obtain system default values for fling, touch, etc...
ViewConfiguration.getMaximumFlingVelocity()
.getMinimumFlingVelocity()
.getScaledTouchSlop()
...

2. Movement Tracking. Talk mentioned: Journey of an event, the Android touch on more details.

@Override public boolean onTouchEvent(MotionEvent event) { 
switch (event.getAction()) {
// Stop animations and start a new drag gesture
case MotionEvent.ACTION_DOWN:
break;
// Move/drag the object according to the pointer
case MotionEvent.ACTION_MOVE:
break;
// Compute the object's velocity and start animating it
case MotionEvent.ACTION_UP:
break;
// The gesture was intercepted by a parent
case MotionEvent.ACTION_CANCEL:
break;
}
return true;
}

3. Inertia scrolling/flinging (after user stops moving finger, scrolling is still happening after some time, slowing down till it stops depends on velocity)

@Override 
public boolean onTouchEvent(MotionEvent event) {
// To measure velocity user is scrolling, initialise
// VelocityTracker
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
// Record every user movement
mVelocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
// When the user lifts her finger, compute velocity, 1000 -
// default value.
mVelocityTracker.computeCurrentVelocity(1000);
// If velocity exceeds your predefined value
if (mVelocityTracker.getXVelocity() > mMinVelocity) {
// Animate the object
}
// no break; Velocity tracker on both cases should be recycled
// as it may be used on another component on system
case MotionEvent.ACTION_CANCEL:
mVelocityTracker.recycle();
mVelocityTracker = null;
break;
}
return true;
}
// To recreate flinging OverScroller can be used. Difference between
// this and Scroller is that the latter does not support
// over-scrolling
public void fling() {
mOverScroller.fling(mStartX, mStartY, // start x/y
mVX, mVY, // velocity x/y
mMinX, mMaxX, // min/max x
mMinY, mMaxY); // min/max y
// Schedule animation on the next scrolling frame
postOnAnimation(mScrollRunnable);
}
private final Runnable mScrollRunnable = new Runnable() {
@Override public void run() {
// The method returns a boolean to indicate whether the scroller
// is finished
if (mOverScroller.computeScrollOffset()) {
final int x = mOverScroller.getCurrX();
final int y = mOverScroller.getCurrY();
// Move object to (x, y).
postOnAnimation(this);
} else {
// animation is over
}
}
};

4. Scrollbars drawing. To allow a user to know where he is in scrolling container.

// Methods must be overriden@Override 
protected int computeVerticalScrollExtent() {
// height of scroll bar handle (extent)
return getHeight();
}
@Override
protected int computeVerticalScrollOffset() {
// height of how much is scrolled
return mOffsetY;
}
@Override
protected int computeVerticalScrollRange() {
// height of all scrolling view
return mContentHeight;
}
// Now when you are actually scrolling you must call
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}

4. Edges feedback. To draw indication on an edge of the screen to hint user that he has reached the edge while scrolling.

// if targeting SDK <14, you can use EdgeEffectCompat
private EdgeEffect mTopEdgeEffect = ...
// to draw edge effect, override draw method
@Override
public void draw(Canvas canvas) {
// allow parent to draw itself first
super.draw(canvas);
// draw only if animation is in progress
if (!mTopEdgeEffect.isFinished()) {
// move the canvas to correct position
canvas.save();
canvas.translate(getPaddingLeft(), 0);
// calculate size of the edge
final int w = getWidth() — getPaddingLeft() — getPaddingRight();
mTopEdgeEffect.setSize(w, getHeight());
if (mTopEdgeEffect.draw(canvas)) {
// Schedule animation on the next scrolling frame
postInvalidateOnAnimation();
}
canvas.restore();
}
}
//Feed EdgeEffect input with these methods// By fling. Higher the velocity, bigger Edge effect will be
mTopEdgeEffect.onAbsorb(int velocity)
// By user scrolling.
mTopEdgeEffect.onPull(float deltaDistance)
// When user user stops scrolling.
mTopEdgeEffect.onRelease()

Nesting Scrolling

Don’t do nested scrolling containers (unless outer and inner has different scrolling axes):

View.canScrollVertically(int) 
canScrollHorizontally(int)

Or you can use techniques like Bezel Scrolling (DrawerLayout) or Boundary Scrolling (ViewPager). Or new Nested Scrolling (introduced in Lollipop, parent steals scrolling events from children), views that support it:

  • ScrollView (SDK 21+)
  • NestedScrollView (SDK 4+)
  • RecyclerView (SDK 7+)
  • ListView (SDK 21+)
  • HorizontalScrollView (not supported)

--

--

Liudas Survila

Android, Clean Code, Software Craftsmanship, UX, Fitness, Science