Colophon
© Copyright 2016-2017 by Adrian Kosmaczewski – All Rights Reserved.
AKOSMA Training Adrian Kosmaczewski
Ringkengässchen 11 – 8200 Schaffhausen – Switzerland
This document is geared towards providing exact and reliable information in regards to the topic and issue covered. The publication is sold with the idea that the publisher is not required to render accounting, officially permitted, or otherwise, qualified services. If advice is necessary, legal or professional, a practiced individual in the profession should be ordered.
In no way is it legal to reproduce, duplicate, or transmit any part of this document in either electronic means or in printed format. Recording of this publication is strictly prohibited and any storage of this document is not allowed unless with written permission from the publisher. All rights reserved.
The information provided herein is stated to be truthful and consistent, in that any liability, in terms of inattention or otherwise, by any usage or abuse of any policies, processes, or directions contained within is the solitary and utter responsibility of the recipient reader. Under no circumstances will any legal responsibility or blame gbe held against the publisher for any reparation, damages, or monetary loss due to the information herein, either directly or indirectly.
Respective authors own all copyrights not held by the publisher.
The information herein is offered for informational purposes solely, and is universal as so. The presentation of the information is without contract or any type of guarantee assurance.
The trademarks that are used are without any consent, and the publication of the trademark is without permission or backing by the trademark owner. All trademarks and brands within this book are for clarifying purposes only and are owned by the owners themselves, not affiliated with this document. Android is a trademark of Google. iOS is a trademark of Apple.
The Android robot is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License.
Published in Switzerland. Created with the eBook Template Toolchain by Adrian Kosmaczewski based on Asciidoctor and PlantUML.
Abstract
This book provides a quick introduction of Android for iOS developers. It targets iOS developers with medium or advanced level, having shipped some iOS applications already in either Objective-C and Swift.
Dedication
To my friend Daniel Steinberg.
Preface
The world of mobile development is a ground in constant motion. However, for the past five years, Android and iOS have both reached the level of dominant players in the field, moving other platforms out of sight. Due to the complexity of these systems, developers tend to concentrate their efforts in just one platform; however businesses must target both platforms to remain competitive in the mobile market.
This book provides an iOS developer’s perspective on Android, highlighting the similarities and the major differences between both platforms. The author hopes that these lines will help other developers to jump to the fascinating world of Android using their hard earned iOS knowledge.
Target Audience
This book is intended as a step-by-step guide to guide developers well versed in the arts of iOS into the realm of Android mobile application development.
How To Read This Book
The author assumes that the reader has never written Android applications before; at most, maybe, she or he has played with an Android device at some point, but nothing else. If you are already familiar with Android, you can skip directly to chapter 2, and start creating apps right away.
If you are not familiar with the Android developer tools, it is strongly recommended to read this book linearly, and to build the sample applications one after the other. This will help you build your skills step by step.
Requirements
This book assumes that the reader is using a Mac – after all, the reader is supposed to be an iOS developer!
It also assumes working programming knowledge in Objective-C or Swift, and of the most common iOS frameworks, such as Foundation, UIKit, Core Location, Core Data and others.
Most importantly, it is also assumed that the reader already knows Java; if not, please be aware that you might have a hard time understanding the code snippets shown in this book. The Bibliography section at the end of the book provides a few useful titles for starting your exploration of Java. In particular I’d recommend reading [Bloch] own "Effective Java" to help you take your Java skills to the next level.
In terms of software requirements, this book assumes that the latest copy of Android Studio is installed in the development machine, as well as Homebrew.
Source Code
The code bundled with the book has been prepared and tested with the latest version of Android Studio; make sure to download and install it in your system before starting.
All applications use the same baseline: API 16, also known as Jelly Bean 4.1. This version of Android was released in July 2012, and at the time of this writing, 97.3% of all Android devices in the wild run a version equal or older to Jelly Bean. This should hopefully give this book the widest possible reach.
Every time that the text of the book references some sample code, a "Follow Along" callout section will appear with the path of the project, which you can open on Android Studio to run the project directly on your device or the emulator:
Follow along
The code of this section is located in the |
Each application is as simple as possible, but not simpler. All the applications are working examples, tested at least in four environments:
-
The official Android Emulator.
-
The Genymotion Android Emulator.
-
A OnePlus 3 Android smartphone.
-
A Samsung Galaxy Tab S2 tablet.
Given the large variety of the Android market, it is possible that some bits and pieces of the source code will not work in some devices; I remember having trouble with some Android devices during my career, so I would not be surprised if some of you encounter difficulties. I will not be able to provide support for your particular device, but I guarantee that the code should work in the environments enumerated above.
Structure
This book is structured around code. The chapters are meant to be read with Android Studio open, in the order they have been written; I have reused bits and pieces of knowledge from previous chapters in many others, so you should be better served by reading them in order.
To help readers get up and running as fast as possible, every chapter features a section called "TL;DR" at its very beginning, including a handy summary of the most important similarities and differences between Android and iOS. You can use the tables in this section as a reference, and if you find them useful you can print a copy of the Appendix C, which contains all the TL;DR tables together in the same place.
The source code included in the book points directly to the applications
available in the code
folder, which contains all the sample applications
bundled as part of this training.
Part 1: Introduction
This first part of the book will guide the reader in the world of Android app development. We will first learn how to install and use Android Studio, we are going to get familiar with the tools and ecosystem, not only to create applications but to be able to debug them effectively.
1. Toolchain
Each platform vendor tries – and, to a large extent, succeeds at – locking third-party developers into their own ecosystem. This is true of many software platforms, and neither iOS nor Android are the exception to this rule.
One of the biggest efforts for iOS developers new to the Android ecosystem is getting used to a new set of tools, paradigms, workflows and even new keyboard shortcuts all over the place. This chapter will present an introduction to the various tools used in the everyday life of a seasoned Android developer.
1.1. TL;DR
As an introduction, these are the most important differences that distinguish the iOS developer experience from that of Android.
Android | iOS | |
---|---|---|
IDE |
Android Studio |
Xcode |
Profiling |
Android Device Monitor |
Instruments |
Preview |
Android Emulator |
iOS Simulator |
Blocks in previous versions |
Retrolambda |
PLBlocks |
Programming Language |
Java |
Swift or Objective-C |
Command Line |
|
|
Going beyond |
Rooting |
Jailbreaking |
Application metadata |
AndroidManifest.xml |
Info.plist |
Dependency Manager |
Gradle |
CocoaPods – Carthage |
Distribution |
APK |
IPA |
Debugger |
ADB + DDMS |
LLDB |
Logger |
LogCat |
NSLog() or print() |
View Debugging |
Hierarchy viewer |
Xcode view debugging |
Static Analysis |
Android Lint |
Clang Static Analyzer |
Classic programming language |
Java |
Objective-C |
Hype programming language |
Kotlin – Groovy – Scala – Clojure |
Swift |
1.2. Java
Android applications are primarily written in Java, a language that shares a lot of features and commonalities with Objective-C and Swift. After all, many Java engineers working for Sun in the nineties came from NeXT, the computer company founded by Steve Jobs that ultimately merged with Apple in 1996, bringing the Objective-C language to the Apple ecosystem.
Both Java and Objective-C share lots of
commonalities, such as protocols (called "interfaces" in the realm of
Java) or primitive boxing types such as NSNumber
(represented by the
family of Number
subclasses in Java.) They also represents two opposite
views in terms of compile-time checks; Objective-C delegates most method
and type resolutions to runtime, while Java performs rather strict type
checking. In this respect, Swift is much more close to Java than
Objective-C will ever be.
Learning Java from Scratch
This book will not provide the reader with an exhaustive introduction to the Java programming language. The Bibliography at the end of this book provides some useful titles for the reader to learn more about it. |
Java has the following characteristics:
-
Single inheritance.
-
Strongly and statically typed.
-
No header files like Objective-C.
-
Objects instantiated in a garbage-collected heap; primitive values on the stack.
-
Classes, interfaces, fields and methods are "package private" by default.
-
The
Object
class is the base class for all objects in Java. -
"Interfaces" are similar to Objective-C and Swift protocols, that is, constructs equivalent to abstract classes with pure virtual methods.
-
Java has no similar concept to that of Objective-C categories or Swift extensions, that is, it is impossible to extend classes other than by inheritance.
-
Full namespace support.
-
The closest equivalent for Objective-C’s
id
or SwiftAny
types is to use cast toObject
. -
Methods can be overloaded, unlike Objective-C but like Swift and other languages.
Of all the items above, the most important is the "Strongly and statically typed" one. This means that method calls in Java are bound at compile time, instead of being dispatched dynamically at runtime like in Objective-C. In spite of this, Java resolves polymorphic methods at runtime using a "vtable," just like in C++ or C#.
Android and Java 8
Android currently only supports version 1.7 of the Java Programming Language. Version 8, which introduced many new features such as lambdas, is not yet completely supported by Android, and at the time of this writing, there has been no official announcement about its availability in the near future. If your application targets Android 7 (API 24) or later, and if your project uses Android Studio 2.1 or later, then you can install "Jack", a new Java compiler which makes some Java 8 features available in Android, such as:
Jack is still an experimental feature which can have conflicts with other tools in the toolchain, so it might be wise to wait until it is officially and fully supported. |
The table "Comparison of Java 1.7, Objective-C 2.0 and Swift 3" provides a comparison between these three languages.
Java 1.7 | Objective-C 2.0 | Swift 3 | |
---|---|---|---|
Inheritance |
Simple, with interfaces |
Simple, with protocols |
Simple, with protocols and protocol extensions |
Semicolons |
Mandatory |
Mandatory |
Optional |
Class definition |
|
|
|
Interfaces |
implements |
conforms to |
conforms to |
Including code |
|
|
|
Class extensions |
no |
Categories |
Extensions |
Dynamic typing |
no |
|
|
Private field suffix |
|
|
|
Memory management |
Garbage collection |
Manual or Automatic Reference Counting |
Automatic Reference Counting |
Generics |
yes (type erasure) |
yes (type erasure) |
yes |
Method pointers |
no |
|
|
Callbacks |
Listeners via anonymous classes |
Delegate objects and blocks |
Delegate objects and functions |
Pointers |
no |
yes |
Via library classes |
Root class |
|
|
|
Visibility |
|
|
|
Exception handling |
|
|
|
Namespaces |
Packages |
Through class prefixes |
Implicit, via modules |
Callbacks
One of the most visible differences between Java and Objective-C & Swift is the syntax for event callbacks. As mentioned above, Java 1.7 does not include lambdas, and as such it uses a feature called "Anonymous Classes" to inline the definition of event handlers and other kinds of callbacks in the code of your applications:
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doSomething();
}
});
In the snippet above, the code attaches an instance of a new "anonymous class" implementing the OnClickListener
interface; all of them (the
subclass and the instance) are just created "on the fly", without the need
for the developer to create a separate file and to override the required
methods there.
iOS developers who remember a time before Objective-C blocks might ask themselves what is the reason for not declaring the class in a separate file, plus an interface file filling the role of a "delegate protocol" between both objects. Rest assured, this approach would be absolutely valid and possible; and in many cases it might even be recommended. However, the anonymous class approach is more idiomatic in Android (that is, you more are likely to encounter it in other projects during your careeer) and it is also extremely convenient for small projects.
The Retrolambda Project
The open source project Retrolambda allows developers to use lambdas in Java 5, 6, and 7. For those who remember the early days of iOS development, PLBlocks used to offer a similar service; being able to use Objective-C blocks in iPhone OS 2.2+ and Mac OS X 10.5 applications. |
As a convenience for the developer, and in anticipation of future versions of Java, Android Studio automatically displays the following shorter syntax for anonymous class callbacks, which mimicks the look and feel of Java 1.8 lambdas, yielding a more readable and "future proof" experience:
mButton.setOnClickListener((v) -> {
doSomething();
});
Getting used to this syntax is going to be very helpful in your path from
iOS to Android. You can switch from one representation to the other by
clicking on the small +
sign in the gutter at the left side of the code
editor.
Learning more about Java Lambdas
To learn more about lambdas and how they compare to anonymous classes, please refer to the official Java documentation by Oracle. |
The Android Runtime
Another important fact about Android is that, by design, the Java applications compiled for the Android operating system are not compatible with a standard Java Virtual Machine – JVM – such as the ones available for Windows, macOS or Linux. This simple fact is often overlooked but it is very important to remember.
Android and the JVM
Compiled Android Java applications are not compatibles with the standard Java Virtual Machine by Oracle. |
Android applications are compiled as DEX binaries (which stands for "Dalvik Executables") and run in a special virtual machine, optimized for mobile devices, formerly known as "Dalvik" and now most commonly referred to as the Android Runtime (ART.) Binaries targeting the ART have the following characteristics:
-
Developers can include Java code bundled in binary form, such as JARs (Java Archives) in their applications; they can also include the source files in their projects, but all of this will be compiled as Android DEX binaries, which has a different binary structure.
-
Not all valid Java APIs for a standard JVM exist under ART; in particular most of the
javax.
packages are unavailable in Android. -
DEX files are smaller than their equivalent JARs.
-
ART uses a register-based architecture, instead of the standard JVM stack-based architecture, in order to increase performance.
-
ART uses non-JVM standard bytecode instructions, and a different inter-process protocol.
-
ART can run several Android applications in the same process if required.
From Android 2.2 "Froyo" to Android 5 "Lollipop", a just-in-time compiler (JIT) had been added to the Dalvik virtual, helping it increase the performance of the final code. ART, on the other hand, single handedly compiles all downloaded apps to native code upon installation, and provides much better garbage collection and debugging facilities than Dalvik.
Compilation
The diagram "Android Application Compilation" shows how close "APK" files are to the equivalent "IPA" files distributed by the Apple App Store. In both cases it consists of a compressed archive containing both the binary of the executable and all of its bundled resources, following a very particular folder structure.
Android Release History
The following table shows the history of Android releases, borrowed from Wikipedia, combined with information from the Android developer dashboard. This information is valid as of December 2016.
Code Name | Version Number | Release Date | API Level | Support status | % |
---|---|---|---|---|---|
Alpha |
1.0 |
September 23, 2008 |
1 |
Discontinued |
– |
Beta |
1.1 |
February 9, 2009 |
2 |
Discontinued |
– |
Cupcake |
1.5 |
April 27, 2009 |
3 |
Discontinued |
– |
Donut |
1.6 |
September 15, 2009 |
4 |
Discontinued |
– |
Eclair |
2.0 - 2.1 |
October 26, 2009 |
5 - 7 |
Discontinued |
– |
Froyo |
2.2 - 2.2.3 |
May 20, 2010 |
8 |
Discontinued |
0.1% |
Gingerbread |
2.3 - 2.3.7 |
December 6, 2010 |
9 - 10 |
Discontinued |
1.3% |
Honeycomb |
3.0 - 3.2.6 |
February 22, 2011 |
11 - 13 |
Discontinued |
– |
Ice Cream Sandwich |
4.0 - 4.0.4 |
October 18, 2011 |
14 - 15 |
Discontinued |
1.3% |
Jelly Bean |
4.1 - 4.3.1 |
July 9, 2012 |
16 - 18 |
Discontinued |
13.7% |
KitKat |
4.4 - 4.4.4 |
October 31, 2013 |
19 |
Security updates only |
25.2% |
Lollipop |
5.0 - 5.1.1 |
November 12, 2014 |
21 - 22 |
Supported |
34.1% |
Marshmallow |
6.0 - 6.0.1 |
October 5, 2015 |
23 |
Supported |
24% |
Nougat |
7.0 - 7.1.1 |
August 22, 2016 |
24 - 25 |
Supported |
0.3% |
1.3. Android Application Startup
When a user taps on the icon of an Android application a whole series of events happen in the device. Many of these events are very similar to those in iOS, and it turns out that, quite unsurprisingly, both operating systems use a very similar architecture, but with quite different class structures backing them.
Let us create a small project in Android Studio. In that project, add
a subclass of the android.app.Application
class, and register that class
as the main application class in your AndroidManifest.xml file. Add two
breakpoints in the source code, one in the Application.onCreate()
method, and another in the MainActivity.onCreate()
method.
The stack traces when hitting both breakpoints is shown below:
training.akosma.startup.StartupApplication.onCreate(StartupApplication.java:8) com.android.tools.fd.runtime.BootstrapApplication.onCreate(BootstrapApplication.java:370) android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1012) android.app.ActivityThread.handleBindApplication(ActivityThread.java:4553) android.app.ActivityThread.access$1500(ActivityThread.java:151) android.app.ActivityThread$H.handleMessage(ActivityThread.java:1364) android.os.Handler.dispatchMessage(Handler.java:102) android.os.Looper.loop(Looper.java:135) android.app.ActivityThread.main(ActivityThread.java:5254) java.lang.reflect.Method.invoke(Method.java:-1) java.lang.reflect.Method.invoke(Method.java:372) com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903) com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
training.akosma.startup.MainActivity.onCreate(MainActivity.java:10) android.app.Activity.performCreate(Activity.java:5990) android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1106) android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2278) android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2387) android.app.ActivityThread.access$800(ActivityThread.java:151) android.app.ActivityThread$H.handleMessage(ActivityThread.java:1303) android.os.Handler.dispatchMessage(Handler.java:102) android.os.Looper.loop(Looper.java:135) android.app.ActivityThread.main(ActivityThread.java:5254) java.lang.reflect.Method.invoke(Method.java:-1) java.lang.reflect.Method.invoke(Method.java:372) com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903) com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
There are several interesting bits of information in the stack traces
above. First of all, the android.os.Looper
class, which as
the name suggests provides the main run loop of the application. Most GUI
toolkits include a similar construction, created at application runtime,
holding an event queue and routing events from the operating system to the
different activities and components of the application.
Looper == NSRunLoop
For all practical purposes, iOS developers will recognize that the
|
If you click on the name of a class in Android Studio while holding down the Cmd key, the IDE will open the corresponding class file; if you do not have the Android source code available in your local workstation the IDE will simply decompile the code from the local SDK and show a stub implementation of the corresponding class, with most of its methods.
By doing this repeatedly, from both the MainActivity
and the
Application
subclass you created previously, you are going to arrive to
the android.content.Context
class, which is arguably the most important
class in the Android SDK. The Context
class includes many different methods,
ranging from file management to database creation to inter-process
communication, and it also holds a reference to the underlying Looper
class.
Equivalent in iOS
There is no similar equivalent of |
The diagram "The android.content.Context class" shows a simplified class hierarchy
of the Activity
and Application
classes and its relationship with the
Looper
class.
Another important element in the stack trace above is Zygote; this piece of software, named after a fertilized living cell, is the application launcher of the Android operating system. To ensure fast application startup, Zygote loads a copy of the ART virtual machine and when a new application launches, forks this VM to sandbox the process in it. If you want to know more about Zygote, this answer in Stack Overflow provides an excellent summary.
1.4. Android Studio
Android Studio is a free IDE provided by Google to develop Android applications. It replaced the venerable Eclipse Android Developer Tools, historically the first official IDE for Android software development for many years. It was announced for the first time in May 2013 at the Google I/O conference. The first stable release was in December 2014. It is available for Windows, macOS and Linux, and is now considered the official IDE for Android development.
End-of-life of the Eclipse Android Developer Tools
Google has announced in November 2nd, 2016 the official end of support and development of the Eclipse Android Developer Tools, which are completely superceded by Android Studio 2.2. |
Android Studio is powered by IntelliJ IDEA, a popular IDE for Java development for the past 15 years. It has a solid reputation, and is particularly appreciated by its advanced support for refactoring, code generation and project navigation features.
Android Studio is available from the Android Studio website. The current version at the time of this writing is 2.2.2; version 2.2 was a major milestone released September 19th, 2016. Android Studio is, by far, the most important piece in the daily workflow of an Android Developer, and includes many different features targeted to simplify the development of Android apps, which like all software development can be quite a complex endeavour sometimes.
Once downloaded and launched, Android Studio will launch a configuration wizard, as shown in Android Studio Setup Wizard – Step 1.
Most developers will choose the standard settings, as shown in image Android Studio Setup Wizard – Step 2.
Finally, Android Studio will automatically download all the elements required for it to work properly, as shown in image Android Studio Setup Wizard – Step 3.
Once Android Studio is ready to go, it will display some tips and tricks every day – something you can easily dismiss if you want, as shown in Android Studio Tips.
Android SDK Environment
Once Android Studio is installed, it is strongly recommended to configure the
environment of your system to point to the folder where the Android SDK
resides. In my system, I have added an # Path for the Android SDK export PATH=~/Library/Android/sdk/platform-tools:~/Library/Android/sdk/tools:"${PATH}" # For Android stuff export ANDROID_HOME=~/Library/Android/sdk |
Creating a New Project
To create a new project in Android Studio , just select the menu:File[New > New Project] menu item, and follow the instructions as shown in the following screenshots.
After running the project wizard, Android Studio should show you a windows similar to the one featured in image New project in Android Studio.
Invoking Android Studio Actions
Instead of clicking your way around in menus, you might want to learn the handy Cmd+Shift+A shortcut; this command allows you to invoke any operation available in the IDE without leaving your hands from the keyboard. |
Command Line Tool
Android Studio allows developers to install a command line utility, useful to open projects directly from a terminal session. Select the Creating a launcher script for Android Studio.
menu entry and select the output folder for the script, as shown in imageOnce installed, just type studio .
at the root of a folder containing an
Android Studio project, and a new windows with the current project will
appear on your screen.
Many of the external tools required to build Android applications are available directly from the Tools/Android Menu in Android Studio.
menu in Android Studio, as shown in imageWe are now going to learn more about each of these pieces individually.
1.5. Kotlin
At the beginning of this chapter I mentioned that Java is the only programming language supported by Google for Android development. The ecosystem, however, includes many other possibilities; at the end of this book, the appendix "Third Party Android Developer Tools" contains a list of other environments and programming languages available to create Android applications. As you will see there, there are lots of different possibilities out there.
In this section we are going to explore one of those options: Kotlin. This programming language was created by the same engineers that created IntelliJ IDEA and Android Studio, and it is uncannily similar to Swift in many ways. Kotlin might be an interesting choice for Swift developers looking to write Android applications using a safe, fast and modern programming language.
Using Kotlin in your Android project requires adding a small library used at runtime for interoperability with Java; this library increases the size of the final application in around one megabyte.
We are going to create a small application using Kotlin, to show how easy it is to integrate and how similar it is to Swift.
Kotlin is available as a plugin for Android Studio. You can manage plugins in Android Studio by opening the Browsing plugin repositories"
menu, as shown in figure "Once the Kotlin plugin is installed, you should restart Android Studio for
it to be loaded in memory. After relaunching, create a simple Android
application and open the MainActivity
class. Select the
menu entry and watch Android
Studio transform the standard Java code of your project into Kotlin.
The listing "Simple activity in Kotlin" shows the resulting code in a very simple application, with a lambda set as the event callback for button clicks.
package training.akosma.kotlin
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
textView.text = "Button clicked!"
}
}
}
Kotlin not only has a syntax that looks a lot like that of Swift, it shares many features with Apple’s new language:
-
Optionals
-
Lambdas
-
Functional programming features
-
Ranges
-
Pattern matching
-
Class extensions
Kotlin compiles its code to native Android bytecode, which means that setting the small overhead of the runtime library aside, an application created with Kotlin behaves and is distributed exactly like one created with Java.
1.6. SDK Manager
As the name implies, the SDK Manager allows the developer to install, manage and uninstall different versions of the Android Software Development Kit (SDK) in the local workstation. Figure Android SDK Manager shows the default state after launching Android Studio and installing the latest available version of Android at the time of this writing, 7.1.1 (also known as Nougat.)
1.7. AVD Manager
The Android Virtual Device or AVD manager allows you to create emulators for your debugging sessions.
Running apps in the Emulator
Once you have created an Android Virtual Device using the AVD Manager, you can assign it to be used for debugging. To do that, you have to first create a "Run/Debug configuration." You can do that directly from Android Studio using the
menu.
Emulator incompatibility with Docker
At the time of this writing, the following message might appear on the Run console of Android Studio when starting the built-in emulator: emulator: ERROR: Unfortunately, there's an incompatibility between HAXM hypervisor and VirtualBox 4.3.30+ which doesn't allow multiple hypervisors to co-exist. It is being actively worked on; you can find out more about the issue at http://b.android.com/197915 (Android) and https://www.virtualbox.org/ticket/14294 (VirtualBox) If this happens to you, make sure you are not running Docker for macOS at the same time, and if this is the case, quit Docker. |
To run your application, just click the Play button on the toolbar. You are going to be asked to select a device (virtual or physical) to run your device into.
Types of binaries
The AVD manager offers both "x86" and "ARM" Android images; it is recommended to install only "x86" images, which run faster in Mac workstations. It is much better to test your app directly on a device, if you need to test the code in an ARM environment. |
Genymotion
As convenient as the built-in Android emulator is, it suffers from several drawbacks:
-
It is very slow. Starting it can take several minutes, depending on the memory available in your machine and the speed of the CPU.
-
It does not work in full-screen mode on macOS.
-
It conflicts with Docker for macOS.
Coping with a slow emulator
Please keep in mind that the default Android emulator can be slow at times, so it is strongly recommended to launch an instance of it and to leave it running while you work on your code. Thankfully, Android Studio 2.2 includes a new feature which allows your code to be deployed much faster to the device or the emulator, and this will help you have shorter code-test cycles. |
Many professional Android developers use the Genymotion emulator instead, which is much faster than the official emulator, offers full-screen mode compatibility to macOS users, and does not conflict with Docker. To use it, you must install VirtualBox from Oracle first.
Free for personal use
Genymotion offers a free download for personal use and evaluation, but it is a commercial developer tool and it is non-free for professional use. Please refer to the Genymotion website for information about pricing and how to buy. |
Genymotion SDK path
If you use the Genymotion Emulator, make sure to open the |
Android Emulator vs. iOS Simulator
At this point, my dear iOS developer reader must be remembering fondly the snappiness and convenience of use of the iOS Simulator. It is important to remember that the words "Emulator" and "Simulator" are not synonyms!
iOS Simulator | Android Emulator | |
---|---|---|
Type of Code |
x86 |
ARM |
Hardware support |
Limited: orientation, memory warnings, UI |
Extended: camera, accelerometer, telephony… |
In short, the iOS simulator allows Xcode to run applications compiled for the x86 architecture to run in a small window that has the shape and size of an iOS device. These iOS applications have access to the whole RAM, disk and operating system features of macOS, but they just happen to run in a window with a strange shape.
The Android Emulator, on the other hand, allows applications compiled for the ARM architecture to run in a well-defined sandbox, with strict memory, disk and networking capabilities, while providing a translation layer for the instructions targeting the ARM architecture to be executed by an x86 CPU.
Without regard for these differences, it is strongly recommended (for both iOS and Android developers) to run and debug their applications in real devices; this will give them a better idea of the performance and the characteristics of their code in the real environment.
1.8. Gradle
It is safe to assert that Android Studio is, under the hood, just a visual environment built around Gradle, an open source build tool created with the Groovy programming language. You can think of Gradle as a tool similar to Maven or Make, but specifically taylored for the task of building and deploying Android applications.
Every Android Studio project includes three default Gradle build files:
-
The
build.gradle
file at the root of the Android Studio project, providing configuration options for all subprojects and modules. -
The
settings.gradle
file, at the root of the project, which specifies the Gradle files to include (by default, only theapp/build.gradle
file.) -
The
app/build.gradle
file, which contains specific instructions and settings for the Android application that will be built by the project.
The latter one specifies compilation parameters, build types, dependencies and many other parameters. The listing The app/build.gradle file shows a typical Gradle build file.
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.0.0"
defaultConfig {
applicationId "training.akosma.introduction"
minSdkVersion 16
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
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:25.0.0'
testCompile 'junit:junit:4.12'
}
Gradle can be invoked from the command line, using the following command:
$ chmod +x ./gradlew $ ./gradlew
The output of running Gradle on the command line looks like this:
To honour the JVM settings for this build a new JVM will be forked. Please consider using the daemon: https://docs.gradle.org/2.14.1/userguide/gradle_daemon.html. Incremental java compilation is an incubating feature. :help Welcome to Gradle 2.14.1. To run a build, run gradlew <task> ... To see a list of available tasks, run gradlew tasks To see a list of command-line options, run gradlew --help To see more detail about a task, run gradlew help --task <task> BUILD SUCCESSFUL Total time: 9.259 secs
To discover the various predefined tasks available in Gradle, just run
$ ./gradlew tasks
This will yield a long lists of tasks.
Learning Gradle
Virtually anything that can be done from the Android Studio IDE can be done from the command line using Gradle. It is recommended to become familiar with this tool, since this knowledge will be helpful to configure build scripts, continuous integration systems, and more. |
1.9. Other Tools
The Android developer life is filled with various tools, each with a specific task. This section will give an overview of the most important of them.
Android Debug Bridge
The Android Debug Bridge (or ADB for short) provides the capability to debug your application from Android Studio in an emulator or in a device connected to the developer workstation. ADB can be used in the command line, to install applications in a device or an emulator, and to launch debugging processes.
ADB is a client-server system that consists of three items:
-
A client, running on your developer machine, used to trigger commands.
-
A daemon, running on the device or the emulator, receiving and responding to commands.
-
A server, running as well in your developer machine, coordinating the communication between client and daemon.
Enable developer options in your device
To enable ADB in your device, you have to enable the USB debugging
option in your device, which is part of the Developer options. On
Android 4.2 and later, you can enable the (otherwise hidden) developer
options by tapping seven times on the |
Developers can debug applications both via USB and via Wifi. Below is a list of useful ADB commands:
-
adb devices
shows the list of available devices. -
adb start
andadb stop
help to start and stop emulator instances. -
adb connect
starts a debugging session on a device connected through a wifi network. -
adb pull remote local
copies theremote
file to thelocal
file. -
adb push local remote
copies thelocal
file toremote
on the device. -
adb shell
starts a shell on the remote device or emulator. -
adb shell screencap /sdcard/screen.png
takes a screenshot of the current display of the device. -
adb shell screenrecord /sdcard/demo.mp4
records the current activity of the device in an MP4 movie.
logcat
logcat is the logger library used in Android apps. It basically replaces
the use of NSLog()
(or print()
in Swift) to output data in the console
while debugging or running Android apps.
Any Android application can log messages to the console by using the following code:
Log.i("application", "This is a message for logcat");
The Log
class is available after importing it:
import android.util.Log;
Hierarchy Viewer
The Hierarchy Viewer is very similar to the View debugger in Xcode. It allows developers to inspect and understand the view tree with all the widgets displayed in the user interface. In Android Studio, select the menu:Tools[Android > Android Device Monitor] menu and select the menu:Window[Open Perspective > > Hierarchy View].
The tree view on the left of the Android Device Monitor window allows you to select the activity you want to inspect (which can be any running application on a device or an emulator.) Select the one that corresponds to your application, and you will be able to see the full view hierarchy on the center of the window.
ProGuard
ProGuard is used to analyze the contents of APK files, in order to reduce their download size, and to make sure that the limit of 65'536 methods is not overridden.
Maximum size for Dalvik executables
Dalvix |
Javadoc
Android Studio can extract the Javadoc documentation included in the source code. Select the menu and the dialog shown in image Javadoc Generation Dialog will guide you.
You can add Javadoc comments very easily, on top of classes, methods, fields and any other Java element:
/**
* Short description.
*
* @param variable Description
* @return Description
*/
public int methodName (...) {
// method body with a return statement
}
1.10. Summary
Google provides a solid set of tools for Android development, including most if not all the tools required to get the job done. Some commercial tools exist as well, and they provide a certain added value to the equation.
Developers spend most of their time in Android Studio, editing and debugging code, both in emulators and on a device. Devices must have "developer mode" enabled in order to enable debugging via USB.
The Android SDK Manager is in charge of the installation and removal of different versions of the Android SDK in the workstation. The Android Virtual Device Manager is used to create emulators with different versions of Android. The Genymotion emulator is a commercial option to the standard Android emulator, offering several functionalities and better performance.
With the knowledge gained during this chapter, we are going to start writing some Android applications, in order to learn how to tie all these tools together.
2. Debugging
Writing software is a difficult activity, and in Android the chances for things to go wrong are multiplied by the astronomical number of devices available in the market. This chapter will show some useful techniques to create, debug and troubleshoot apps in different environments and with different tools.
2.1. TL;DR
For those of you in a hurry, the table below summarizes the most important pieces of information in this chapter.
Android | iOS | |
---|---|---|
Debugger |
JDB |
LLDB |
Log output |
logcat |
Xcode console |
Remote debugging |
yes |
no |
Log viewers |
PID Cat & LogCat |
libimobiledevice & deviceconsole |
Network logger |
NSLogger |
NSLogger |
2.2. Enabling Exception Breakpoints
The first thing that I recommend you to do is to enable exception breakpoints, either caught or uncaught. This will enable Android Studio to stop the execution of the application in case an Exception occurs, and given the wide range of possibilities for errors, this can be a handy measure before any debugging session starts.
To do that, just open the Shift+Cmd+F8 keyboard shortcut) and check the "Java Exception Breakpoint" and the "Any Exception" checkboxes, as shown in image "Breakpoints Window in Android Studio."
(or hit the2.3. Enabling USB Debugging
If you have reached this point in the book one could imagine very well that you have been able to successfully run code in your device; but for those readers who have jumped directly to this section, here is a quick recap of the steps required to debug applications in your device.
First, you must enable "Developer mode" in your device. Open the Settings application and scroll to the "About phone" section. In that screen you should see a "Build number" entry, which you must tap seven times. If you try to do it again, you should see a "toast" message just like the one shown in image Developer mode already active.
After you have done that, the Settings application will display a new "Developer" item. Select it, scroll down and you will see a toggle switch to enable USB debugging in the device, as shown in image USB Debugging switch.
2.4. Enabling WiFi Debugging
The Android debugger can also be used via a standard WiFi connection, although this can be a security problem and you should remember to disable this capability when you are done.
First connect your device with the USB cable and make sure that ADB is running in a specific port:
$ adb tcpip 5555
To enable WiFi debugging, then issue the following command, to retrieve the IP address of the device:
$ adb -s e99b50ed shell ifconfig
The common output of the ifconfig
command returns information for each
of the interfaces available in the device; normally the one labeled
wlan0
is the one you are looking for. Write down the IP address,
disconnect the USB cable and then run the next command:
$ adb connect 192.168.1.xxx:5555
The adb devices
command should show something similar to the following
now:
$ adb devices -l List of devices attached 192.168.1.xxx:5555 device product:OnePlus3 model:ONEPLUS_A3003 device:OnePlus3
If you see the above, you can now use the device from Android Studio or any other IDE, and you will be able to debug your application just as if it were running tethered through the standard USB cable.
At the end of your debugging session, make sure to type the command to disable wifi debugging in your device.
$ adb -s 192.168.1.xxxx:5555 usb
This is a security measure, to disable any attempts in any network to debug and inspect applications in your device. After running this command, any attempt to connect to your device should print the following output:
$ adb connect unable to connect to 192.168.1.xxx:5555: Connection refused.
Some devices offer a menu entry in the Developer settings called "ADB over network" which serves the same purpose, allowing you to enable and disable the setting visually, as shown in image ADB over network option (Source: stackoverflow.com/a/10236938/133764). |
2.5. Working on the Command Line
I am a bit of a command line junkie, and I like being able to perform as many tasks as possible using my preferred macOS terminal tools: iTerm 2, zsh and tmux. In this section we are going to see how easy it is to leave Android Studio aside for a while, and use command line tools to create, build and debug Android applications.
Android tool no longer supported
This section references the old |
First, open your preferred terminal and make sure that you have all the required tools on your system:
$ env | grep ANDROID ANDROID_HOME=/Users/adrian/Library/Android/sdk
$ which android /Users/adrian/Library/Android/sdk/tools/android
$ which adb /Users/adrian/Library/Android/sdk/platform-tools/adb
$ which ant /usr/local/bin/ant
If you do not have ant
installed, you can easily install it using
Homebrew: brew install ant
.
Now, let us create a simple Android application:
$ android create project -n CommandLineApp -t 'android-16' -p CommandLineApp -k training.akosma.commandline -a MainActivity
This command creates a project named CommandLineApp, targeting API 16 or
later (the same baseline API level we have been using for this whole book)
in the folder CommandLineApp, using the ID training.akosma.commandline
and including a default activity class named MainActivity
.
After running the project, inspecting the contents will show you the structure of the generated code:
$ ls CommandLineApp AndroidManifest.xml ant.properties bin build.xml libs local.properties proguard-project.txt project.properties res src
The build.xml
file is a standard ant build file, so we can start issuing
commands to build and install the application as required.
$ cd CommandLineApp $ ant debug
This last command builds the application in "debug" mode; as you might
expect, the ant release
command does the same in "release" mode, ready
to be signed and deployed to the Play Store. Finally, ant clean
removes
all built binaries and other compilation products.
You can run the ant command just by itself to see a list of
available tasks in the current build.xml file.
|
Let us edit the code of the application first. You can use any editor you
want, of course, but in my case I will stick to my beloved vim
editor,
which does a great job as far as I am concerned.
Open the CommandLineApp/res/layout/main.xml
file and edit the
android:text
property of the autogenerated TextView
instance inside of
the main layout, until the code looks like the listing
Layout of a command line app.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World from the command line!"
/>
</LinearLayout>
Let us also enable debugging in the application, by adding the
android:debuggable="true"
attribute to the application
tag in the
AndroidManifest.xml
file at the root of the project:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="training.akosma.commandline"
android:versionCode="1"
android:versionName="1.0">
<application android:label="@string/app_name"
android:icon="@drawable/ic_launcher"
android:debuggable="true">
<activity android:name="MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Shameless plug: if you are interested in my vim configuration, please check out my dotfiles project in Github. |
Let us install the application in the device. First, connect your Android device to your Mac and run the following command to verify that ADB is connected to it:
$ adb devices -l List of devices attached e99b50ed device usb:337772544X product:OnePlus3 model:ONEPLUS_A3003 device:OnePlus3
If your device does not appear, just plug and unplug the USB cable of the device, and make sure that you have enabled USB debugging in the device. |
Of course, in your case the output will be different; this is what I see when I connect my own OnePlus 3 device with the USB cable.
To install the debug build in my device, I just have to run the following command:
$ ant -Dadb.device.arg="-s e99b50ed" debug install
The -Dadb.device.arg
parameter requires the device ID seen in the adb
devices -l
command output.
Alternatively, you can also install your application using the adb
command:
$ adb -s e99b50ed install bin/CommandLineApp-debug-unaligned.apk
Taking screenshots using adb
As mentioned previously in this book, you can easily take screenshots using ADB with the following commands: $ adb -s e99b50ed shell /system/bin/screencap -p /sdcard/screenshot.png $ adb -s e99b50ed pull /sdcard/screenshot.png screenshot.png |
Once the application is installed, how about debugging it? The Android toolkit allows you to debug applications entirely through the command line. The ADB process can bridge debugger commands to the Android Runtime, by the means of port forwarding. Let us see how to do that.
Launch the application in your device and then retrieve the process ID in your device:
$ adb -s e99b50ed jdwp 27496
JDWP stands for
Java
Debug Wire Protocol, a standard defined for Java debuggers, and which the
Android platform implements, albeit in limited form. The technique we are
going to use basically uses ADB to connect a local instance of jdb
(the
standard Java debugger distributed with the Java SDK) to the process
running in the device.
The last command we ran returns the number of the process in the Android Runtime of the device. We are going to use that number to connect all the pieces together.
First, let us set a debug bridge between jdb
and the device:
$ adb forward tcp:7777 jdwp:27496
Then, launch the Java debugger and start a debugging session:
$ jdb -sourcepath src -attach localhost:7777
A short debugger session then looks more or less like this:
Set uncaught java.lang.Throwable Set deferred uncaught java.lang.Throwable Initializing jdb ... > stop in training.akosma.commandline.MainActivity.OnCreate Unable to set breakpoint training.akosma.commandline.MainActivity.OnCreate : No method OnCreate in training.akosma.commandline.MainActivity > stop in training.akosma.commandline.MainActivity.onCreate Set breakpoint training.akosma.commandline.MainActivity.onCreate > Breakpoint hit: "thread=main", training.akosma.commandline.MainActivity.onCreate(), line=12 bci=0 12 super.onCreate(savedInstanceState); main[1] list 8 /** Called when the activity is first created. */ 9 @Override 10 public void onCreate(Bundle savedInstanceState) 11 { 12 => super.onCreate(savedInstanceState); 13 setContentView(R.layout.main); 14 } 15 } main[1] next > Step completed: "thread=main", training.akosma.commandline.MainActivity.onCreate(), line=13 bci=3 13 setContentView(R.layout.main); main[1] list 9 @Override 10 public void onCreate(Bundle savedInstanceState) 11 { 12 super.onCreate(savedInstanceState); 13 => setContentView(R.layout.main); 14 } 15 } main[1] help ** command list ** <SNIP> main[1] cont > quit
The previous listing shows several debugger commands:
-
stop in
to create a breakpoint. -
list
to show the current status of the instruction pointer upon hitting a breakpoint. -
next
to "step over" to the next instruction. -
cont
to continue the execution. -
help
to learn more about other commands.
Finally, you can import projects created with the command line from Android
Studio; just select the import-summary.txt
with details about
the import process.
2.6. Using logcat and PID Cat
If you use the command line frequently, you will start missing the logcat output displayed by Android Studio at the bottom of the IDE. If that is the case, and you are using (as you should) logcat commands in your application, you should install then PID Cat, a logging tool that provides color output in the terminal, and which can be restrained to only display the logs for the application you are interested in.
PID Cat fulfills the same role as libimobiledevice or deviceconsole for iOS.
You can install it very easily using Homebrew: brew install pidcat
. Once
installed, just call it using this command:
$ pidcat training.akosma.pidcatexample
The console should display something similar to the screenshot Example output with PID Cat.
The PID Cat help text shows that the tool allows to filter entries by verbosity level or by device or emulator, among other options.
$ pidcat --help
usage: pidcat [-h] [-w N] [-l {V,D,I,W,E,F,v,d,i,w,e,f}] [--color-gc] [--always-display-tags] [--current] [-s DEVICE_SERIAL] [-d] [-e] [-c] [-t TAG] [-i IGNORED_TAG] [-v] [-a] [package [package ...]] Filter logcat by package name positional arguments: package Application package name(s) optional arguments: -h, --help show this help message and exit -w N, --tag-width N Width of log tag -l {V,D,I,W,E,F,v,d,i,w,e,f}, --min-level {V,D,I,W,E,F,v,d,i,w,e,f} Minimum level to be displayed --color-gc Color garbage collection --always-display-tags Always display the tag name --current Filter logcat by current running app -s DEVICE_SERIAL, --serial DEVICE_SERIAL Device serial number (adb -s option) -d, --device Use first device for log input (adb -d option) -e, --emulator Use first emulator for log input (adb -e option) -c, --clear Clear the entire log before running -t TAG, --tag TAG Filter output by specified tag(s) -i IGNORED_TAG, --ignore-tag IGNORED_TAG Filter output by ignoring specified tag(s) -v, --version Print the version number and exit -a, --all Print all log messages
This is an invaluable tool to be able to quickly scan for specific values in the logger output of your applications. A similar tool, but this time with a graphical user interface for macOS is LogCat.
2.7. Using NSLogger
Although PID Cat is useful, sometimes it is not possible or desirable to debug application instances individually using the USB cable. For more complex debugging scenarios, NSLogger is a useful option.
NSLogger allows developers to gather logging information from running applications in the local network. To do that, developers must include the NSLogger NSLogger Android client in their applications. When the application runs, the code automatically tries to send all of its logging information to an application running on a Mac on the same local network as the devices. Developers can then watch live the updates of their applications and thus to troubleshoot problems in specific devices.
To install NSLogger in your own app, at the time of this writing there is no simple mechanism (like, for example, using Gradle.) The code of NSLogger must be included in the application, but since the linkage is done through static methods and properties, the Java compiler can strip the code out of the application if not used (for example, in Release builds.)
Download the zip from the main Github
repository of NSLogger and copy the folder inside of the Client
Logger/Android/client-code/src
into the app/src/main/java
folder of your own
application. Android Studio should detect the new code automatically.
Copy the files in the Client Logger/Android/example/com/example/
folder using
Finder and paste them in Android Studio as part of your application.
Add the following permissions to your application in the AndroidManifest.xml file:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
Include your logging code, either in your custom subclass of the Application
class in your project, or in your activity, to activate logging, as shown in
Sending NSLogger calls.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (Debug.D) {
Debug.enableDebug(getApplication(), true);
// change to your mac's IP address, set a fixed TCP port in the Prefs in desktop NSLogger
Debug.L.setRemoteHost("192.168.1.108", 50007, true);
Debug.L.LOG_MARK("MainActivity startup");
}
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Debug.L.LOG_UI(0, "Button clicked");
}
});
SeekBar bar = (SeekBar) findViewById(R.id.seekBar);
bar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
Debug.L.LOG_APP(0, "SeekBar changed");
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
Debug.L.LOG_NETWORK(0, "SeekBar onStartTrackingTouch");
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
Debug.L.LOG_SERVICE(0, "SeekBar onStopTrackingTouch");
}
});
}
}
Change the IP address to match the one where the client NSLogger application is running. |
Before launching your application in Android Studio, make sure to download and launch the NSLogger desktop viewer application in your Mac. You should configure so that the port used by the client code is the same as the one used in the desktop viewer, as shown in image NSLogger desktop viewer configuration.
Having done this, and after adding some logging calls in your code, launching and using the application should automatically open a new NSLogger window in your Mac, displaying something similar to the contents of image NSLogger application running.
The contents of the NSLogger macOS application window can be saved into
a file with the .nsloggerdata extension. The source code of this book
includes a sample NSLogger file in the Debugging/NSLogger folder.
|
2.8. Summary
Android applications can not only be debugged on the Android Studio IDE, but also on the command line; remember that Android Studio can be seen as a huge user interface built on top of Gradle and ADB.
Other open source libraries, such as NSLogger and PID Cat, provide additional services for inspecting the behavior of applications in different devices.
Part 2: User Interfaces
Good looks are fundamental for any successful application, and this is true for both iOS and Android. The good news is that both system share a lot of commonalities, including drawing APIs that looks incredibly similar. This part will explain the Android graphics subsystem, including the view hierarchies, the APIs and other topics.
3. User Interface
Android uses the same basic input interface as iOS; a touchscreen. Through this interface, users are able to manipulate and interact with widgets such as buttons, scrollbars, panes and menus, with a sense of physicality very much like the one offered by UIKit and its related frameworks.
In this chapter we are going to learn how to build and organize user interfaces in our applications, concentrating our attention in the major building blocks of Android apps: Activities, Intents and Fragments.
3.1. TL;DR
For those of you in a hurry, the table below summarizes the most important pieces of information in this chapter.
Android | iOS | |
---|---|---|
UI design |
Layout files |
NIB/XIB/Storyboard |
Controllers |
Activity |
UIViewController |
Callbacks |
Anonymous Classes |
|
Views |
|
|
Connecting views |
|
|
Text fields |
|
|
Buttons |
|
|
Text labels |
|
|
Translatable strings |
strings.xml |
Localizable.strings |
Navigation between controllers |
Intent |
Storyboard Segue |
UI decomposition |
Fragment |
Children UIViewController |
Serialization |
|
|
Dialog boxes |
|
|
3.2. UI Design Guidelines
Material Design is the current visual language that Google has created to unify the visuals and interactions throughout their complete suite of products, on the web, on the desktop and of course on mobile devices.
This book is definitely not a book about visual design (and if you can tell through my UML diagrams, I can say that design in general is not one of my strenghts!) but it is important to understand the underlying principlies behind Material Design.
Google created Material Design with the following principles in mind:
A material metaphor is the unifying theory of a rationalized space and a system of motion.
The foundational elements of print-based design – typography, grids, space, scale, color, and use of imagery – guide visual treatments.
Motion respects and reinforces the user as the prime mover.
Of course, this should come as no surprise to any seasoned iOS developer; Apple itself is well known for having created visual guidelines for their own operating systems (starting with macOS and following with iOS, watchOS and tvOS) for a long time.
These guidelines, as the name suggest, provide designers and developers with a common language, enabling teams to discuss and elaborate visual architectures for their applications.
I strongly suggest the reader of these lines to spend some time browsing the Google Material Design website in order to understand the paradigms and the ideas behind the different visual elements that make up an Android application.
3.3. The Support Library
Historically, the characteristic of Android that frightens most iOS developers is the sheer diversity of devices and versions of Android available in the wild. The technical press usually refers to this issue as the "fragmentation" of the Android world… but as with many things in the press these days, it is safe to say that these claims (and the fears generated as a consequence) are overrated.
Early in the development of Android, Google realized that application developers should be able to support lots of different devices, with different screen sizes (such as tablets and smartphones of all sizes) and resolutions. This situation led to two very important additions to the Android toolkit back in 2011:
-
Fragments.
-
The Support Library.
The Support Library, initially known as the Android Compatibility Package, allows applications running in older versions of Android to enjoy the UI paradigms and the features brought to the system in new versions of Android.
This library is available to Android developers through the Android SDK, and is distributed to users through the Play Store; this means that Android devices that include the standard Google Play Store will always have the Support Library installed, and this enables all applications to run seamlessly in all devices, starting in Android 2.3 (API level 9) and higher.
Throughout this book, we are going to use this library extensively. The
package is named android.support
and all of our applications will
inherit from android.support.v7.app.AppCompatActivity
instead of the
standard Activity
class provided by Android.
3.4. Activities
By far, the most important building block of Android applications are
activities. They fulfill a similar role to that of UIViewController
instances in iOS, that is, to manage the display of a screenful of data
at any single time.
Android Studio allows developers to build applications around the Activity paradigm, creating and removing activities for each of the specific tasks that are required by the application. Activities are a powerful architectural mechanism for organizing and encapsulating your code.
Activities must be self-contained and have as few dependencies from other activities as possible. This will allow you to reuse them in the same or in other applications easily. |
The Activity Stack
In every Android application, there is a default activity that is launched and displayed by default when the application starts. This is similar to the "entry point" defined in iOS Storyboards. The same way you can move the arrow that represents the entry point of the Main storyboard in an iOS application, you can specify the default activity for any Android application as a simple entry in the AndroidManifest.xml file:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="training.akosma.basic">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
However, Android being different from iOS, it also means that the way activities are connected to each other is also different. In Android, all activities are included by default in a default navigation stack , and any activity can be launched independently of the other activities bundled in an application.
For iOS developers, it is better to think that the operating system holds
a large UINavigationController
instance that spans accross all
applications; every time the user launches an application, the activity is
"pushed" to this global navigation stack. When the user presses the
Back button (which is usually available in most Android devices by
default as a hardware button) the current activity is "popped" from the
stack, and the device returns to the previous state of operation,
whichever that is.
This means that if your application launches an activity from the calendar application, and this one in turn launches another activity, say for example a contact from the address book, then when the user presses the back button twice, the current activity will be again your own activity, the one that started this chain of operations.
A Basic Application
In the source code bundled with this book, please open the Android Studio
project located in the UI/Basic
folder. This is a very simple Android
application with a single activity, that shows how to use an activity as
a simple controller for the UI of your application.
To recreate the application by yourself, follow these steps:
-
Create a new Android Studio application. Select the default options, including the "Empty Activity" template.
-
When Android Studio is ready, you open the
activity_main.xml
layout file and delete the label that appears on top of the screen. -
Using your mouse, drag three components to the UI, in the following order: a plain text EditText, a Button, and a TextView widget. You should see them in the palette at the left side of the editor panel, a familiar sight for those used to Interface Builder.
-
Select the
EditText
component in the designer, and on the right side of the editor you should see a properties panel. Change the ID tonameEditText
, change the "hint" property toEnter your name and touch "Greet"
and remove the value of the "text" property. -
Select the
Button
component in the designer, and on the properties panel change the properties as follows: set the ID togreetButton
, set the text toGreet
. -
Select the
TextView
component in the designer, and set the properties as follows: ID togreetingTextView
and remove any value in the "text" property.
As you can see, the Android UI designer properties panel works in
a very similar way as that of Interface Builder; it organizes the
properties following the inheritance chain; the EditText properties
appearing on top of those defined in the TextView class.
|
-
Launch your application on the emulator or, better yet, connect your device to your Mac and use it to launch the application. You should see the widgets displayed according to the layout created in the designer, but for the moment you can not do anything on the user interface. Clicking the Greet button yields nothing. It is time to enter some code.
-
Double click on the
MainActivity.java
file in the project browser on the left of the Android Studio window (if you have not done that already.) We are going to add a bit of code to that file, until it looks like the source code in MainActivity.java
public class MainActivity extends AppCompatActivity {
private TextView greetingTextView;
private Button greetButton;
private EditText nameEditText;
(1)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
(2)
greetingTextView = (TextView) findViewById(R.id.greetingTextView);
greetButton = (Button) findViewById(R.id.greetButton);
nameEditText = (EditText) findViewById(R.id.nameEditText);
(3)
greetButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String name = nameEditText.getText().toString();
String greeting = "Hello, " + name;
greetingTextView.setText(greeting);
}
});
}
}
1 | Just like UIViewController instances, Activities have a well-defined
lifecycle . They have several methods that are
called at specific moments in their lifetime. This method, onCreate() is
roughly equivalent to viewDidLoad in iOS, as it is also called when the
instance is created in memory. |
2 | Android, unlike iOS, does not make automatic connections between the
code and the widgets. The R class, however, is generated
automatically and provides strongly typed references to the widgets
defined in the activity_main.xml file. We have to manually wire the
widgets into the application, so that the Activity can modify and query
their state. |
3 | As mentioned in the previous chapter, the version of Java used in
Android is version 7, and not the latest one, version 8, which introduced
the concept of lambdas. For that reason, in order to add event handlers to
our buttons and widgets, we have to use the concept called "Anonymous
Classes." In this example we are subclassing and instantiating on the
fly a View.OnClickListener instance, and we also override the method
that will be called when the event is registered by the UI. |
The R class is autogenerated, and can sometimes get out of sync
with the resources of the project. If that happens, a quick solution is to
use the menu item in Android Studio.
|
The views returned by the calls to the findViewById() method,
as you can see in the snippet MainActivity.java, are referenced
simply as View instances, and the developer must pay attention in
casting those views to the correct subview classes. Failure to do so may
result in runtime crashes!
|
Activity States
As you have just seen, Activities have
a lifecycle very similar to that of the UIViewController
class. The
following diagram shows in detail the various states and methods called in
each state change, as the Activity instance is created, modified and
disposed.
Inspecting the UI
When inspecting the user interface using the Hierarchy Viewer bundled in the Android Device Monitor, the screen shown in image Inspecting the user interface of the Basic application appears.
Android Widgets
In the Basic application built above, we have used several different widgets for the user interface.
Sometimes Android UI widget classes have strange sounding names for those of us coming from the iOS world. The naming conventions are different, and as such it is important to learn them. |
In the "Basic" application above we have used the TextView
, EditText
and Button
classes, which are all related through inheritance, as shown
in the diagram EditText Hierarchy Diagram.
The TextView
class is a subclass of android.view.View
, the base class
of all visible things in Android. Just like in iOS, a View is
a representation of a rectangle on the screen, including everything that
is drawn inside.
Instances of the android.view.View class, unlike UIView
instances, cannot have children widgets; this is only possible at the
level of the android.view.ViewGroup class, itself a subclass of
android.view.View . Remember this when you will want to create your own
complex view systems.
|
Many of the most important subclasses of android.view.View
are shown in
the diagram View Hierarchy Diagram.
Finally, when a developer wants to set or get the text of a TextView
, it
turns out that the setter and getter do not take String
instances, but
rather reference the CharSequence
interface. In the case of the
EditText
, the getText()
method returns an object implementing the
Editable
interface.
As a general design decision, using interfaces in your method signatures it is always a good idea. It makes your APIs more flexible and extensible. |
Optimizing the Project with Lint
Let us enhance now this "Basic" application a little bit. First of all, we are going to select the Android Studio Code Inspection Dialog will appear.
menu item. The dialog shown in imageAndroid Lint is a tool roughly equivalent to the Clang Static Analyzer, available in Xcode in the menu item. We will run it with the default options, and among the many improvements, there are a few "low hanging fruits" which we can fix right now.
The output of the tool appears at the bottom of the screen, as shown in the image Android Studio Lint Results.
The "Inspection" pane at the bottom of the Android Studio window contains the list of problems found in the project, and when selecting any of these items, an explanation is shown on the left side.
String Files
Now let us fix some of these problems.
The first one is actually highlighted in the image Android Studio Lint Results,
and has to do with the fact that we have hardcoded strings in the layout
file when we created our "Basic" application. This is hardly a good idea;
first of all, we might want to make our application available to users in
other languages in the future, and this is where the strings.xml
file
comes in handy.
The strings.xml
resource file is more or less equivalent to the
Localizable.strings
file used in Cocoa to provide international
versions of all the strings included in the application. There is,
however, a nice difference; the strings referenced in strings.xml
are
immediately parsed by Android Studio, and they are available through the
autogenerated R
class.
To solve the problem in the Lint Inspection pane, select it with your
mouse and hit the Cmd+↓ keyboard shortcut. This will open the file
where the problem resides and will scroll automatically to the required
line of code. Replace the value Enter your name and touch "Greet"
with
the text @string/edit_hint
. At first Android Studio will complain that
the key is non existent, but we are going to fix that immediately.
Open now the strings.xml
file; for that, let us use another handy
keyboard shortcut: Shift+Cmd+O. This opens a "Quick Open" dialog
that allows you to open any file on the project. Add the required key on
the file, and then do the same with the text on the Greet button.
Your strings file should now look like shown in listing A simple strings.xml file.
<resources>
<string name="app_name">Basic</string>
<string name="edit_hint">Enter your name and touch "Greet"</string>
<string name="greet_button">Greet</string>
</resources>
One of the nice things of concentrating strings inside of the resources file is that Android Studio proposes to "autocomplete" most string placeholders with values taken out of it. This simplifies the handling of these values greatly, and makes it easy to translate all the strings in a project in one operation, usually before publication.
One final note about string files:
to avoid (mis)handling the XML code in the file, Android Studio provides
a nice interface that can be used to edit strings instead. Image
Android Studio Translation Editor shows this, which can be accessed by clicking on
the "Open editor" link that appears on the top right of the strings.xml
file tab in Android Studio.
Handling Orientation Changes
Whether you are using now a physical device or an emulator, try now the
following: first, enter your name in the EditText
field, hit the
Greet button and then change the orientation of the device.
See what happened? The value in the TextView
suddenly disappeared. This
is something that can be very puzzling for iOS developers. In iOS, the
state of a UIViewController
is kept between orientation changes, and the
controller just receives a few callbacks to be notified of the change.
In the case of Android, the behavior is radically different; when the user rotates the device, the activity is destroyed and disposed, and a new one is created and displayed.
The only possible solution for an Android developer, given these constraints, is to save the current state of the application, and to reload it accordingly if required. Let us add some code to solve this problem.
private static final String KEY = "greeting";(1)
if (savedInstanceState != null) {(2)
greetingTextView.setText(savedInstanceState.getString(KEY));
}
@Override(3)
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
String value = greetingTextView.getText().toString();
outState.putString(KEY, value);
}
1 | This is the key used to store the current value of the TextView
instance when the activity is about to be destroyed. |
2 | When Android creates the activity, it verifies whether there was already a stored value; if this is the case, then use it to reset the UI to the previous state. |
3 | This method is called right before the Activity is destroyed; we use
the KEY string to store the current value of the TextView widget
before being called upon oblivion. |
3.5. Intents
The basic communication mechanism between activities is the
android.content.Intent
class. Whenever an activity wants to start
another activity, or to communicate with another process in the device, it
will always use an instance of the Intent
class.
This architecture has no equal in the world of iOS applications, where the communication between view controllers is usually strongly coupled; this has the advantage of a simpler programming model, relatively easier to understand for newcomers, but it also leads to tangled architectures, where it is almost impossible to reuse controllers in different contexts.
Thanks to intents, Android activities can truly be independent from each other at every time, which helps in the creation of decoupled architectures, with high degrees of testability and reuse.
Follow along
The code of this section is located in the |
There are three use cases for intents in Android:
-
Explicit intents specify the class of the activity to launch, and are commonly used inside of a single application to navigate from screen to screen.
-
Implicit intents are used to open system-wide services, such as asking the built-in browser to open a web page or to search for a contact in the contacts database.
-
Return values from an activity to the "previous" one in the activity stack are also
Intent
instances, holding on to the data that must be passed back.
Let us learn now how to use Intent
instances to open other activities.
Implicit Intents
Implicit intents are the simplest. Just specify the action you would like
to launch (in this case, android.content.Intent.ACTION_VIEW
) and the
parameter (in this case, a URL.) The intent shown in listing
"Using implicit intents" shows how to create and start an implicit
intent, which has the net result of opening the default web browser in the
device.
mWebButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(ACTION_VIEW, Uri.parse("http://akosma.com"));
startActivity(intent);
}
});
The startActivity()
method of the Activity
class takes an Intent
instance as a parameter, and asks the operating system to do the rest.
Explicit Intents
The simplest use case consists in navigating from one activity to another, that is, pushing a new activity on the device navigation stack. The listing "Using explicit intents" shows how to do that.
mAgeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent(MainActivity.this, AgeActivity.class);
i.putExtra("age", mAge);
startActivityForResult(i, 0);
}
});
In this case we use the startActivityForResult()
method of the
Activity
class, because we are expecting the AgeActivity
class to
return a simple value.
Returning Values
The AgeActivity
class in our example contains a SeekBar
that the user
can slide from left to right to choose a suitable age. When the user
presses the "Back" button (or, alternatively hits the "Finish" button) the
current activity is popped off the current stack and the setResult()
method is called. This method takes an Intent
as a parameter, one that
contains the data to be passed to the previous activity.
private void notifyAge() {
Intent data = new Intent();
data.putExtra("age", mAge);
setResult(RESULT_OK, data);
}
When an activity calls setResult()
the one that has requested it through
the startActivityForResult()
will be notified of this, and the
onActivityResult()
callback will be activated.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != RESULT_OK) {
return;
}
mAge = data.getIntExtra("age", DEFAULT_AGE);
displayAge();
}
Thanks to this simple architecture, data can flow from one activity to another freely and simply. The structure of the data is, of course, part of an implicit contract that should be documented and specified – and tested, if at all possible.
Another example of requesting data from the operating system is shown in listing "Requesting and showing a contact", where we ask the user to select a contact from its device and we display the name on our application.
mContactsButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
startActivityForResult(intent, PICK_REQUEST);
}
});
@Override
public void onActivityResult(int request, int code, Intent data) {
super.onActivityResult(request, code, data);
if (request == PICK_REQUEST
&& code == Activity.RESULT_OK) {
Uri contactData = data.getData();
ContentResolver resolver = getContentResolver();
Cursor c = resolver.query(contactData, null, null, null, null);
if (c.moveToFirst()) {
String column = ContactsContract.Contacts.DISPLAY_NAME;
int index = c.getColumnIndex(column);
String name = c.getString(index);
mTextView.setText(name);
}
}
}
Data Providers
The contacts example we saw in the previous section is just one example among a large collection of generic data providers. We are going to learn more about Android data providers in Chapter Storage. |
3.6. Fragments
Remember iOS "Universal Applications"? These are, according to Apple, iOS applications that can run both in the "form factor" of the iPhone or on that of the iPad. They are usually quite easy to create on iOS; just create different storyboards for each device, wire the scenes in your storyboards accordingly, and iOS takes care of the rest. If the application is running on an iPhone, then the iPhone storyboard will be displayed; if it is on an iPad, then the iPad storyboard will be taken into account.
Android works in pretty much the same way; the difference is that Android applications can have different layout files for different screen sizes, and even for different resolutions, orientations, and many other factors!
However, no matter how many layout files you have in your projects, Activities still take all the available screen space, every single time. Android does not allow many activities to share the current screen. Hence, a different solution was required. And this solution is called fragments.
Fragments are one of the most fundamental visual building blocks in Android. They allow developers to create flexible user interfaces that work differently in different devices, yet they are distributed as the same application in the Play Store.
Fragments are, in a sense, like small UIViewController
instances that are
children of a bigger, "container" UIViewController
. You can compose complex
user interfaces in iOS by nesting controllers inside of other controllers. Each
contains its own view logic, and talks to other controllers using well-defined
interfaces (like notifications, delegate protocols or other mechanisms.)
In Android, Fragments always exist inside an Activity. Activities can have one or many fragments, each containing its own view logic.
Follow along
The code of this section is located in the |
We are going to create now a small application that displays the same data in
different ways depending on whether it is running on a tablet or on
a smartphone; we are actually going to create something similar to
a UISplitViewController
!
The first step consists in creating the individual fragments for our application. We need a fragment that displays a list of items, and another fragment that displays just one of the items; the classical "master & detail" user interface paradigm.
We are also going to need two activities; one is the MainActivity
, the root
activity of the application, whether it is running on a smartphone or a tablet.
If the application is running on a tablet, it will display both fragments at
the same time. If it is running on a smartphone, that means that the "detail"
fragment will not be visible, and then the DetailActivity
, itself containing
the DetailFragment
will be called using an intent.
These are the layouts we need for both activities; as you can see, there are two layouts for the MainActivity, which has two different "look & feels":
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment class="training.akosma.fragments.ListFragment"
android:id="@+id/list_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment class="training.akosma.fragments.ListFragment"
android:id="@+id/list_container"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<fragment class="training.akosma.fragments.DetailFragment"
android:id="@+id/item_container"
android:layout_width="0dp"
android:layout_weight="2"
android:layout_height="match_parent" />
</LinearLayout>
The DetailActivity
class only needs one layout:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_detail"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="training.akosma.fragments.DetailActivity">
<fragment
class="training.akosma.fragments.DetailFragment"
android:id="@+id/item_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
In terms of code, the most complex class in the project is the ListFragment
class, which actually uses a android.support.v7.widget.RecyclerView
instance
to display a list of strings. The RecyclerView
class is the closest thing in
Android to an iOS UITableView
, and we are going to learn more about it in the
next few chapters. For the moment you only need to know that the Adapter
class inside of it is used as a data source, just as you would do it in iOS.
The ListFragment
class also defines a "callback protocol" so that users of
that fragment are notified of events on the RecyclerView
. The root activity
of the project is responsible for the coordination of the work between the
fragments, and we can see that at work in listing
"The MainActivity class coordinating the fragments".
@Override
public void onItemSelected(String value) {
Toast.makeText(this, value, Toast.LENGTH_SHORT).show();
DetailFragment detailFragment = (DetailFragment) getSupportFragmentManager().
findFragmentById(R.id.item_container);
if (detailFragment == null || !detailFragment.isInLayout()) {
Intent intent = new Intent(this, DetailActivity.class);
intent.putExtra(DetailFragment.PARAMETER, value);
startActivity(intent);
} else {
detailFragment.update(value);
}
}
As you can see, here we check for the existence of the detail fragment in the
layout of the activity; if we are running the app in a smartphone, then the
layout will not include that fragment, and the check will yield a falsy value;
hence, we just create an Intent
and ask the operating system for a new
activity, in order to display the value that the user selected on the list.
On the other hand, if the app is running on a tablet, then the layout file that has been loaded by the operating system already includes that fragment, and thus the only thing we need to do is to simply update its value.
Coordination via LocalBroadcastManager
In the previous example we used a direct communication pattern between the
list fragment and its host activity. You can use the
android.support.v4.content.LocalBroadcastManager
class for a more disconnected system of
interaction, one that is suspiciously similar to the one provided in Cocoa
by the NSNotificationCenter
and NSNotification
classes.
Local broadcast are available through the support library, and they guarantee privacy since no app can accept local broadcasts from any other application, and no other application can listen to the local broadcast of other apps.
Follow along
The code of this section is located in the |
The first thing we need to do is to make the MainActivity
class, the one
that coordinates the communication between fragments, to register itself
as a listener of a local broadcast.
Create an android.content.BroadcastReceiver
object and override its
onReceive()
method, as shown in listing
"A local broadcast receiver."
private BroadcastReceiver mMessageReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
receive(intent);
}
};
private void receive(Intent intent) {
String value = intent.getStringExtra(Constants.DATA_KEY);
Toast.makeText(this, value, Toast.LENGTH_SHORT).show();
DetailFragment detailFragment = (DetailFragment) getSupportFragmentManager()
.findFragmentById(R.id.item_container);
if (detailFragment == null || !detailFragment.isInLayout()) {
Intent showDetailIntent = new Intent(this, DetailActivity.class);
showDetailIntent.putExtra(DetailFragment.PARAMETER, value);
startActivity(showDetailIntent);
} else {
detailFragment.update(value);
}
}
Then register this receiver object in onCreate()
, as shown in listing
"Registering a local broadcast receiver."
LocalBroadcastManager manager;
manager = LocalBroadcastManager.getInstance(this);
manager.registerReceiver(mMessageReceiver,
new IntentFilter(Constants.NOTIFICATION_NAME));
Needless to say, you should deregister it on onDestroy()
, as shown in
listing "Deregistering a local broadcast receiver."
@Override
protected void onDestroy() {
LocalBroadcastManager manager;
manager = LocalBroadcastManager.getInstance(this);
manager.unregisterReceiver(mMessageReceiver);
super.onDestroy();
}
Finally, let us modify the ListFragment
class so that local broadcasts
are sent every time that the user taps on an item, as shown in
"Sending a local broadcast."
@Override
public void onClick(View view) {
Intent intent = new Intent(Constants.NOTIFICATION_NAME);
intent.putExtra(Constants.DATA_KEY, mItem);
Context activity = getActivity();
LocalBroadcastManager manager;
manager = LocalBroadcastManager.getInstance(activity);
manager.sendBroadcast(intent);
}
3.7. Summary
Activities are the basic building block of Android applications. Every
activity manages a screenful of information, just like UIViewController
instances would on iOS. The UI of activities can be designed visually,
just like with Interface Builder in Xcode.
Developers can attach event handlers to UI widgets directly using anonymous classes, which are the Java 1.7 equivalent of Lambdas, which have only been introduced in Java 1.8, and are not yet available in Android for the time being.
Intents are objects used to launch other activities, either in this application or in any other application in the system. They provide the glue that allow applications to talk to each other.
Applications should be built around Fragments, to ensure their adaptation to other device sizes, such as tablets. In that sense, Activities should be seen as containers for fragments, allowing them to collaborate and to share information at runtime.
Finally, the whole operating system works like a giant
UINavigationController
instance, onto which Activity instances are
constantly pushed and popped, and this explains the importance of the back
button in Android devices.
4. Graphics
Android applications, just like their iOS counterparts, rely heavily in strong, bold, beautiful graphics to convey their meaning and to help users perform their tasks. In this chapter we are going to learn the basics of Android graphics – which, as we will see, are very similar to those of iOS – as well as a few techniques to gather complex events and drawing intricate graphics in the easiest possible way.
4.1. TL;DR
As usual, for those readers in a hurry, the table below summarizes the most important pieces of information in this chapter.
Android | iOS | |
---|---|---|
Framework |
|
UIKit |
Views |
|
|
Coordinate system |
Origin at top left |
Origin at top left |
Location on screen |
|
|
Images |
|
|
Colors |
|
|
Bezier curves |
|
|
Drawing method |
|
|
Drawing context |
|
|
Mark as "dirty" |
|
|
Gestures |
|
|
Pinch gesture |
|
|
Affine Transformations |
|
|
Simple animations |
|
|
Complex animations |
|
|
Application-level memory warnings |
|
|
Activity-level memory warnings |
|
|
4.2. Graphics on Android
Let us begin our discussion about drawing on Android devices with a little bit of theory. This section will explain the coordinate system used in Android graphics, the units, the variety of screen densities, and will provide some background about how to solve some common memory problems that arise when handling large amounts of data.
Coordinate system
To draw, we not only need to know what to draw, but where to draw it. In
iOS, the coordinate system has its origin – the (0, 0)
point – at
the top left of the screen. In Android, it is exactly the same.
Follow along
The code of this section is located in the |
To learn more about the Android coordinate system we are going to create a small project using API 17 as a basis – we are going to break our rule of using API 16 for this one – and we are going to modify the application so that the Activity takes the whole screen; no status bar, no action bar, nothing. Just the activity.
To do that, no need to add any code; the XML resource files are enough. First
we are going to modify the base application theme in res/values/styles.xml
so
that it looks like listing "Fullscreen application style".
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="windowNoTitle">true</item>
<item name="windowActionBar">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
We are going to take the basic style called Theme.AppCompat.Light.NoActionBar
provided by Android and we are going to personalize it, removing the title bar
and the action bar as required.
We are also going to remove all padding from the activity, modifying the
res/values/dimens.xml
and /res/values-w820dp/dimens.xml
files:
<resources>
<dimen name="activity_horizontal_margin">0dp</dimen>
<dimen name="activity_vertical_margin">0dp</dimen>
</resources>
Our activity will contain a single TextView
centered in the middle of the
screen, and we are going to programmatically add other TextView
instances on
the screen, using something that all iOS developers know too well: absolute
positioning.
int width = 300;
int height = 300;
Display display = getWindowManager().getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
display.getRealMetrics(metrics);
int maxX = metrics.widthPixels;
int maxY = metrics.heightPixels;
int density = metrics.densityDpi;
/*
Point size = new Point();
display.getSize(size);
int maxX = size.x;
int maxY = size.y;
*/
label.setText(String.format("Size: %d x %d px\nDensity: %d dpi", maxX, maxY, density));
RelativeLayout layout = (RelativeLayout) findViewById(R.id.activity_main);
TextView topLeftTextView = new TextView(this);
topLeftTextView.setText("Top Left");
RelativeLayout.LayoutParams topLeft = new RelativeLayout.LayoutParams(width, height);
topLeftTextView.setBackgroundColor(Color.BLUE);
topLeftTextView.setTextColor(Color.WHITE);
topLeftTextView.setTypeface(null, Typeface.BOLD_ITALIC);
topLeftTextView.setTextSize(20f);
layout.addView(topLeftTextView, topLeft);
The final result of this operation is visible in figures "Screen dimensions in a smartphone" and "Screen dimensions in a tablet", which are real screenshots taken in devices running the application.
Do not use absolute positioning
The code above only serves the purpose of showing the sizes and position of
elements on the screen, but views on Android screens should always be
positioned relatively or using some kind of flow layout, like |
Units
The previous exercise shows values on the screen, in pixels and in dots per inch. Just like in iOS, the drawing system in Android is device independent, and as such it can accomodate and work in devices of all kinds. The following list provides the complete reference of units supported in Android.[1]
- px
-
Pixels - corresponds to actual pixels on the screen.
- in
-
Inches - based on the physical size of the screen. 1 Inch = 2.54 centimeters
- mm
-
Millimeters - based on the physical size of the screen.
- pt
-
Points - 1/72 of an inch based on the physical size of the screen.
- dp or dip
-
Density-independent Pixels - an abstract unit that is based on the physical density of the screen. These units are relative to a 160 dpi screen, so one dp is one pixel on a 160 dpi screen. The ratio of dp-to-pixel will change with the screen density, but not necessarily in direct proportion. Note: The compiler accepts both "dip" and "dp", though "dp" is more consistent with "sp".
- sp
-
Scale-independent Pixels - this is like the dp unit, but it is also scaled by the user’s font size preference. It is recommend you use this unit when specifying font sizes, so they will be adjusted for both the screen density and user’s preference.
The table "Android Graphical Units" shows the relationship and major characteristics of these units.
Unit | Description | Units Per Physical Inch | Density Independent | Same Physical Size On Every Screen |
---|---|---|---|---|
px |
Pixels |
Varies |
No |
No |
in |
Inches |
1 |
Yes |
Yes |
mm |
Millimeters |
25.4 |
Yes |
Yes |
pt |
Points |
72 |
Yes |
Yes |
dp |
Density Independent Pixels |
~160 |
Yes |
No |
sp |
Scale Independent Pixels |
~160 |
Yes |
No |
Use dp whenever possible
As a rule of thumb, always privilege the use of density-independent pixels (dip) whenever possible, just like you would use points instead of pixels in iOS. |
Screen densities
The following tables show the "classical" Android screen densities;
please keep in mind that most modern Android smartphones and tablets fall
into the xhdpi
and xxhdpi
classes, offering incredibly high levels of
contrast and display. You should ideally test your app in the largest
possible number of devices to ensure that your designs scale well into new
size classes.
Density Bucket | Screen Density | Physical Size | Pixel Size |
---|---|---|---|
ldpi |
120 dpi |
0.5 x 0.5 in |
0.5 in * 120 dpi = 60x60 px |
mdpi |
160 dpi |
0.5 x 0.5 in |
0.5 in * 160 dpi = 80x80 px |
hdpi |
240 dpi |
0.5 x 0.5 in |
0.5 in * 240 dpi = 120x120 px |
xhdpi |
320 dpi |
0.5 x 0.5 in |
0.5 in * 320 dpi = 160x160 px |
xxhdpi |
480 dpi |
0.5 x 0.5 in |
0.5 in * 480 dpi = 240x240 px |
xxxhdpi |
640 dpi |
0.5 x 0.5 in |
0.5 in * 640 dpi = 320x320 px |
About color warmth and other issues
Designing beautiful graphics is a subject that falls outside of the scope of this book, but please keep in mind that different devices feature different screen technologies, and as such the warmth and gamut of the colors could vary sensibly from the drawing application to the device. Again, the only solution here is to test in a wide range of devices, from different manufacturers. |
Out of memory when using graphics
One of the most common problems when working with graphics in Android is
the dreaded, never welcome, drastically horrid OutOfMemoryError
,
also referred to as "OOME" in the Java literature. Given the tight
conditions in which Android applications run, it is entirely possible
– actually, extremely likely – that you will encounter one of these errors
in your life as a graphical Android developer. Allocate a huge
android.graphics.Bitmap
instance in memory, and you could end the life
of your application right away.
The first and simplest solution could be to… well, load a smaller version of the same image. This advice seems silly but remember that most Android devices do not have big screens, and loading that 20 MB image all of a sudden could not be the best idea, particularly if a 50 KB version of the same image offers the same quality to the end user.
If you still have to load large images in memory, there are a couple of tricks you can use.
-
You can add the
android:largeHeap="true"
property to yourAndroidManifest.xml
file. This will tell the operating system to pay attention to the fact that your application might need more memory than usual. However, doing this can slow down your application in some devices, because more memory means longer (and potentially more frequent) garbage collection cycles, which impact performance directly. -
Use
java.lang.ref.SoftReference<>
objects to hold your Bitmaps. SoftReference objects are guaranteed to be removed from memory before the system throws the dreaded OOME. -
Resample the image using
BitmapFactory.decodeResource()
(and, of course, save that image afterwards in the local storage of your device for it to be reused later.) -
Implement the
onTrimMemory()
method in yourActivity
subclasses; this method is the equivalent of thedidReceiveMemoryWarning()
method of theUIViewController
class in UIKit. -
Add a subclass of the
android.app.Application
to your project, register it in yourAndroidManifest.xml
file and implement theonLowMemory()
method, which is the equivalent of theapplicationDidReceiveMemoryWarning(_:)
method of theUIApplicationDelegate
protocol in UIKit.
Follow along
The code of this section, showing how to use |
Checking for the current memory status of your device is quite straightforward, as shown in listing "Checking the current memory status of your device."
ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();
String mem = humanReadableByteCount(memoryInfo.availMem, false);
String message = String.format("Available memory: %s", mem);
if (memoryInfo.lowMemory) {
message += "\nLow memory!";
}
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle("Memory status").setMessage(message);
AlertDialog dialog = builder.create();
dialog.show();
Listing "Receiving and reacting to a memory warning" shows what kind of action to take when an activity receives low-memory warnings.
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
switch (level) {
case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
// Called when the app went to the background
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
// These happen at runtime, even if your app is
// on the foreground. If CRITICAL, the operating
// system will begin to kill processes.
break;
case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
// This is even worse; this process is in the
// list to be terminated as soon as possible.
// This might be your last chance to survive.
break;
default:
// This is a generic low-memory level message.
// Do your job. Release memory. Now.
break;
}
}
Listing "Reacing to a memory warning on the Application subclass" shows some "last resort" measures, asking the operating system to trigger the JVM garbage collection. Not precisely a good idea in itself, but useful for the curious among you.
public class LargeBitmapApp extends Application {
@Override
public void onLowMemory() {
super.onLowMemory();
System.runFinalization();
Runtime.getRuntime().gc();
System.gc();
}
}
Remember to declare your custom Application
subclass in the
AndroidManifest.xml
file! Otherwise it will not be taken into account,
as shown in listing "Declaring a custom Application class and extending the JVM heap size."
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:largeHeap="true"
android:name=".LargeBitmapApp">
4.3. Custom Views
This part of the book is the one that I like the most: we are going to learn how to draw anything on the screen of an Android device by creating a simple drawing application. This application will have two very simple features:
-
Users can choose a color and a brush size
-
They can draw freely on the screen.
The most important point in this whole application is that the final code will
be very small – no more than 100 lines of code for the View
class where the
drawing takes place!
Follow along
The code of this section is located in the |
You can see a screeshot of the application in action in figure "Drawing application on a tablet."
The application consists of a single Activity which does not do anything
else than wiring events to the different components in the UI; we have
a Button
, a SeekBar
to select sizes, and a couple of RadioButton
instances to select the color of the brush.
The core of the application is undoubtedly the DrawableCanvas
class,
itself a subclass of android.view.View
.
public class DrawableCanvas extends View {
private int mColor = Color.BLACK;
private float mStrokeWidth = 3.0f;
private Line mCurrentLine;
private ArrayList<Line> mLines = new ArrayList<>();
public DrawableCanvas(Context context, AttributeSet attrs) {
super(context, attrs);
}
DrawableCanvas
is a subclass of android.view.View
, the most
important class in the drawing system of Android. Every single component
you can see on most Android apps (with the exception of those apps drawing
their own widgets, like OpenGL games for example) is an instance of
a subclass of View
.
And the good news for iOS developers is that View
is very similar to
UIView
in many ways. The biggest similarity is the fact that both
include an overridable method that can be used to perform custom drawing
on the screen; in the case of iOS it’s the UIView.draw()
method; in the
case of Android is the View.onDraw()
method. Even the names are
similar!
Listing "[src-draw-ondraw]" shows how we are going to draw lines on the
screen, using the android.graphics.Path
, which is in many ways the same
thing as a UIBezierPath
on iOS.
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (Line line : mLines) {
Path path = line.getPath();
Paint paint = line.getPaint();
canvas.drawPath(path, paint);
}
}
As you can see, the onDraw()
method shares many points in common with
its iOS counterpart:
-
This method is never called directly by the user; the application
Looper
decides when and how to refresh the screen. The task of the developer is merely to include the drawing code, and nothing else. -
The method receives an instance of
android.graphics.Canvas
as a parameter, which looks suspiciously similar to aCGContext
parameter received byUIView.draw()
. Both represent an in-memory structure where visual changes can be made, and the Android operating system will take those changes and pass them to the GPU (if the current device has one) or the graphics subsystem, in order to refresh the screen. In this case we are callingcanvas.drawPath()
which, as the name implies, draws a particular path object on the screen, using aandroid.graphics.Paint
object, with informatio about color, stroke width and other details.
Of course, the lines must be drawn by the user, and the DrawableCanvas
class should react to the touch events generated by the user. We are going
to override yet another method, in this case View.onTouchEvent()
which
reminds us of similar methods in the UIResponder
class.
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result = super.onTouchEvent(event);
if (!result) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mCurrentLine = new Line(mColor, mStrokeWidth);
mLines.add(mCurrentLine);
mCurrentLine.moveTo(event.getX(), event.getY());
invalidate();
return true;
case MotionEvent.ACTION_MOVE:
if (mCurrentLine != null) {
float x = event.getX();
float y = event.getY();
mCurrentLine.lineTo(x, y);
}
invalidate();
return true;
case MotionEvent.ACTION_UP:
if (mCurrentLine != null) {
mCurrentLine = null;
}
invalidate();
return true;
}
}
return result;
}
The code above is quite straightforward, but there is one very interesting
method called inside of that switch
statement: the
View.invalidate()
method is the exact equivalent of
setNeedsDisplay()
in iOS. It tells the operating system that the current
instance is "dirty" and that it should be redrawn as soon as possible,
usually at the end of the current Looper
iteration.
4.4. Persisting the State of Views
In chapter User Interface, more exactly in section "Handling Orientation Changes" we talked about how Android activities must save their state when the orientation of the screen changes; and that this is actually a very good idea for dealing with low memory situations, in which your application might be killed to free memory for other processes.
It turns out that this requirement applies not only to activities, but
also to all the views contained in that activity. The
View.onSaveInstanceState()
and View.onRestoreInstanceState()
are
called automatically in all views, so that their state can be safely
restored in the case of an orientation change or an application restart.
Listing "Saving the state of views" shows how our DrawableCanvas
class is able
to save and restores its own state whenever the hosting activity suffers
from some kind of destruction event.
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable("superState", super.onSaveInstanceState());
bundle.putParcelableArrayList("lines", mLines);
return bundle;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
mLines = bundle.getParcelableArrayList("lines");
super.onRestoreInstanceState(bundle.getParcelable("superState"));
}
}
The most important concept of saving application state revolves around the
android.os.Parcelable
interface. Parcelable specifies the methods
to implement to make any object subject to serialization and
deserialization in the case of a destruction event.
The most common class implementing Parcelable
is the android.os.Bundle
class, which is simply a bag of string keys and Parcelable
values.
In our drawing application we have implemented the Parcelable
interface
in our Line
and Point
classes; these are used by the DrawableCanvas
class to store the information about the lines created by the user as the
finger moved around on the screen.
Architecture of the Draw application
In the "Draw" application, the |
Listing "Implementing the Parcelable interface" shows how the Line
class implements
Parcelable
, which involves overriding two methods and adding one public
static final field called CREATOR
(this might be the most puzzling fact
about the Parcelable
interface, by the way.)
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
PointF[] points = new PointF[mPoints.size()];
mPoints.toArray(points);
parcel.writeInt(mColor);
parcel.writeFloat(mStrokeWidth);
parcel.writeParcelable(mInitialPoint, 0);
parcel.writeParcelableArray(points, 0);
}
public static final Creator<Line> CREATOR = new Creator<Line>() {
@Override
public Line createFromParcel(Parcel parcel) {
Line line = new Line(parcel.readInt(), parcel.readFloat());
PointF initialPoint = (PointF) parcel.readParcelable(PointF.class.getClassLoader());
line.moveTo(initialPoint.x, initialPoint.y);
PointF[] points = (PointF[]) parcel.readParcelableArray(PointF.class.getClassLoader());
for (int i = 0; i < points.length; ++i) {
PointF point = points[i];
line.lineTo(point.x, point.y);
}
return line;
}
@Override
public Line[] newArray(int i) {
return new Line[i];
}
};
Thanks to this system, the MainActivity
can be destroyed at wish, and if
this happens the state of the DrawingCanvas
will be restored without
problem.
Generating code for the Parcelable interface
Android Studio includes plugins that automatically generate the code required by the Parcelable interface! You can install them in the settings of the application, under the section "Plugins." |
4.5. Gestures
Let us be very clear from the beginning; Android does not include anything
remotely similar to the beloved UIGestureRecognizer
family. This
means that all interactions on the screen must be managed manually,
a situation somewhat similar to the world of iPhone development before
iPhone OS 3.2 (released together with the first iPad, and which included
gesture recognizers for the first time.)
Follow along
The code of this section is located in the |
Having to deal with multiple touch patterns in code is not simple, but
listing "Multi-touch in Android" shows one possible way to do this. The
activity must keep track at all times of the state of the touches, and
using this information it builds an instance of android.graphics.Matrix
,
which in many ways is the Android equivalent of CGAffineTransform
matrices.
@Override
public boolean onTouch(View v, MotionEvent motionEvent) {
ImageView view = (ImageView) v;
switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mSavedMatrix.set(mMatrix);
mOriginalPoint.set(motionEvent.getX(), motionEvent.getY());
mState = DRAG;
mLastEvent = null;
break;
case MotionEvent.ACTION_POINTER_DOWN:
mOldDistance = spacing(motionEvent);
if (mOldDistance > 10f) {
mSavedMatrix.set(mMatrix);
midPoint(mMidPoint, motionEvent);
mState = ZOOM;
}
mLastEvent = new float[4];
mLastEvent[0] = motionEvent.getX(0);
mLastEvent[1] = motionEvent.getX(1);
mLastEvent[2] = motionEvent.getY(0);
mLastEvent[3] = motionEvent.getY(1);
mDistance = rotation(motionEvent);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
mState = NONE;
mLastEvent = null;
break;
case MotionEvent.ACTION_MOVE:
if (mState == DRAG) {
mMatrix.set(mSavedMatrix);
float dx = motionEvent.getX() - mOriginalPoint.x;
float dy = motionEvent.getY() - mOriginalPoint.y;
mMatrix.postTranslate(dx, dy);
} else if (mState == ZOOM) {
float newDist = spacing(motionEvent);
if (newDist > 10f) {
mMatrix.set(mSavedMatrix);
float scale = (newDist / mOldDistance);
mMatrix.postScale(scale, scale, mMidPoint.x, mMidPoint.y);
}
if (mLastEvent != null && motionEvent.getPointerCount() == 2) {
float newRot = rotation(motionEvent);
float r = newRot - mDistance;
float[] values = new float[9];
mMatrix.getValues(values);
float tx = values[2];
float ty = values[5];
float sx = values[0];
float xc = (view.getWidth() / 2) * sx;
float yc = (view.getHeight() / 2) * sx;
mMatrix.postRotate(r, tx + xc, ty + yc);
}
}
break;
}
view.setImageMatrix(mMatrix);
return true;
}
Gesture recognizers on Android
At least one project in Github is trying to bring an implementation of gesture recognizers to the world of Android development, but as far as the author of these lines is concerned, this is probably the only effort of this kind at the time of this writing. |
4.6. Animations
Another thing that Android makes as easy to use as in iOS are
animations. All classes inheriting from View
have a set of
"animatable properties" that can be… well, animated! We are going to learn
how to do this in this section.
Follow along
The code of this section is located in the |
There are basically two main APIs that allow you to animate views on the screen, and they happen to be extremely similar to their counterparts in UIKit.
The first API is the View.animate()
method, which is analogue to
the animate(withDuration:animations:)
method of UIView. Using this
method you just specify the duration and the transitions that you require,
and you can attach a callback object (implementing the
Animator.AnimatorListener
interface) to be notified of different events
in the life of the animation.
Listing "Simple animations using View.animate()" shows how to animate a simple
TextView
instance.
float transparency = 1.0f;
if (mVisible) {
transparency = 0.0f;
}
mVisible = !mVisible;
mTextView.animate()
.alpha(transparency)
.setDuration(mDuration)
.setListener(this);
The second API is the family of android.animation.Animator
classes, which is extremely similar to the CAAnimation
family of classes
in the Core Animation framework of iOS; for example, ObjectAnimator
is
similar to CAPropertyAnimation
; AnimatorSet
is similar to
CAAnimationGroup
and so on.
Diagram "Animator Hierarchy Diagram" shows the class hierarchy of the
Animator
family of classes.
Listing "More complex animations using Animator" shows how to use the Animator
classes in your code.
ValueAnimator rotate = ObjectAnimator.ofFloat(mTextView,
"rotation", 0f, 360f);
ValueAnimator moveH = ObjectAnimator.ofFloat(mTextView,
"translationX", 0f, 100f);
ValueAnimator moveV = ObjectAnimator.ofFloat(mTextView,
"translationY", 0f, 100f);
ValueAnimator backH = ObjectAnimator.ofFloat(mTextView,
"translationX", 100f, 0f);
ValueAnimator backV = ObjectAnimator.ofFloat(mTextView,
"translationY", 100f, 0f);
AnimatorSet set = new AnimatorSet();
set.setDuration(mDuration)
.play(rotate)
.before(backH).before(backV)
.after(moveH).after(moveV);
set.addListener(this);
set.start();
But, what is animatable in a View
? Well, just like with iOS, there is
a defined set of "animatable properties" that can be… well, animated!
These are the properties:
- translationX and translationY
-
Location of the view in its container.
- rotation, rotationX, and rotationY
-
Rotation of the view around its pivot point.
- scaleX and scaleY
-
Scaling of the view around its pivot point.
- pivotX and pivotY
-
Location of the pivot point (by default it is at the geographical center of the view.)
- x and y
-
Coordinates of the view in the reference frame of its parent view.
- alpha
-
Transparency of the view, ranging from 0 (transparent) to 1 (fully opaque.)
Finally, you can be notified of the end of an animation, whether it is
a simple or a complex one, by passing an object implementing the
Animator.AnimatorListener
interface and providing a suitable
implementation of its methods, as shown in listing
"Notification after the end of an animation"
@Override
public void onAnimationEnd(Animator animator) {
if (mVisible) {
mMenuItem.setTitle("Disappear");
} else {
mMenuItem.setTitle("Appear");
}
Toast.makeText(MainActivity.this,
"Animation finished!",
Toast.LENGTH_SHORT)
.show();
}
4.7. Using PaintCode
To close this chapter, I wanted to provide all mobile developers out there with a closer look at an amazing commercial application called PaintCode. Originally targeting only iOS developers, version 3 (released during the writing of this book) included the possibility to generate Java code, helping designers and developers to work closer when creating cross-platform user interfaces.
PaintCode generates code for Android, iOS (both in Swift and in Objective-C), macOS (in Swift and in Objective-C) the web (generating JavaScript Canvas, CSS and SVG code) and even in C# for the Xamarin application programming environment. It can generate "stylesheets" in the shape of classes, including all the required resources such as colors, gradients, shadows and images, so that your application shares common design elements throughout platforms.
Figures "The PaintCode application in action" and "[img-paintcode-device]" show how an admittedly bad design is translated faithfully from the designer application to the device.
I can only recommend downloading the trial and giving it a shot; you might be surprised of the possibilities. But please make sure to have a designer in the team, or at least do not hire me to do your designs!
4.8. Summary
Android provides a fully fledged set of APIs, ready to bring you craziest user interfaces to life. The variety of devices and resolutions available in the market, however, require you to pay attention and to make sure that your designs will scale gracefully in all kinds of hardware.
Drawing custom views in Android is very similar to using Core Graphics in iOS,
including the existence of a "context" object onto which all drawing is
performed. Similarly, developers must not call the onDraw()
method
themselves, as this is the job of the operating system.
Views can save their state automatically whenever their container activity is being destroyed. They should do this to ensure that the user experience they provide stays untouched by device orientation changes, memory warnings or other situations.
Finally, Android has full support for animations, and once again this subsystem is incredibly similar to that of iOS. Developers can use applications such as PaintCode to help them create their designs with greater fidelity accross platforms.
Part 3: Managing Data
Arguably, getting and manipulating data are the most important tasks of any application in any operating system. In this part we are going to learn how to connect to data sources through the network, how to store that data locally and how to display it in the screen of our devices.
5. Networking
In this chapter we are going to learn how to use some very common
networking technologies used by Android applications to communicate with
servers and with other devices on the Internet. Not only that, but we are
also going to learn how to display the data from the network in a list
similar to a UITableView
.
5.1. TL;DR
For those of you in a hurry, the table below summarizes the most important pieces of information in this chapter.
Android | iOS | |
---|---|---|
Native networking library |
|
|
Background mechanism |
|
|
JSON Parser |
|
|
JSON (de)serialization |
Gson |
|
XML SAX |
|
|
XML DOM |
|
KissXML |
Array |
|
|
Table view |
|
|
Table view data |
|
|
Table view cell |
|
|
REST Client |
Retrofit |
RESTKit |
Popular networking library |
OkHttp |
AFNetworking |
5.2. Consuming REST Web Services
We are going to start this chapter by creating a very simple application that performs a network request to a free API provided by the GeoNames geographical database. The data of this database is freely available, and it is licensed under a Creative Commons Attribution 3.0 License.
In particular, we are going to use a very nice API they offer, called "findNearbyWikipedia" which returns items of interest located in a geographical region. This API returns data in both JSON and XML formats, which we will use to show how to parse data in these two different formats.
First we are going to use the default HTTP libraries offered by Android, and later in this chapter we are going to use Retrofit, a third party open source library created by the team of Square.
Built-in HTTP Libraries
We are going to start our exploration of Networking in Android by performing a very simple GET request to one of the endpoints in the GeoNames APIs.
Follow along
The code of this section is located in the |
Create a new project in Android Studio, using all the default parameters.
Add a new Java Class to this project and name it APIConnector
. The code
of the APIConnector
class is available in the code snippet
Class performing a GET HTTP request.
public class APIConnector {
public byte[] getData(String urlString) throws IOException {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setUseCaches(true);
conn.setRequestMethod("GET"); // default value
conn.setConnectTimeout(30000);
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
InputStream in = conn.getInputStream();
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException(conn.getResponseMessage() + " (" + urlString + ")");
}
int read = 0;
byte[] buffer = new byte[1024];
while ((read = in.read(buffer)) > 0) {
out.write(buffer, 0, read);
}
out.close();
return out.toByteArray();
}
finally {
conn.disconnect();
}
}
public String getStringData(String urlString) throws IOException {
return new String(getData(urlString));
}
}
The HttpURLConnection
class in the Android SDK fulfills a similar
role to that of the NSURLConnection
class in Cocoa. You can specify
various parameters, including the HTTP verb to be used (by default, as one
might expect, this value is GET
) and other parameters, such as headers,
cookies, authentication, etc.
Use the built-in cache
Remember how caching was one of the most complex problems in computer
science? Do not create your own local cache for downloaded images! Just
use the one provided by the |
We are going to use now this simple wrapper around the HttpURLConnection
class in our MainActivity
. We could simply do the following at this
stage:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String data = new APIConnector().getStringData(API_URL);
}
But this approach has a major flaw; it runs the network connection on the main thread of the application. And this is a bad, bad thing; just like it is in iOS. No surprise here.
Android applications, like most GUI toolkits out there, use an "event
loop" in the main thread. This event loop, represented by the Looper
class, and very well known to any iOS developer used to see the
NSRunLoop
class out there in the wild, performs pretty much the same
tasks in Android as it does in iOS. It consumes events from the operating
system, and executes the code associated to these events as fast as
possible. If we use the APIConnector
class in our main loop, it will
block its execution until the network call has completed (or failed for
any reason, including a timeout.) We say that the network call is executed
synchronously.
What we need, in this case, is a mechanism to execute our network call in
a background thread, or asynchronously, and for this reason we are going
to use the standard android.os.AsyncTask
class.
A much better version of the MainActivity
class looks like this:
public class MainActivity extends AppCompatActivity {
private final static String API_URL = "http://api.geonames.org/findNearbyWikipediaJSON?formatted=true&lat=47&lng=9&username=USERNAME&style=full";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new ConnectToAPITask().execute();
}
private class ConnectToAPITask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... voids) {
try {
String url = API_URL.replace("USERNAME", "steve");
String data = new APIConnector().getStringData(url);
Log.i("MainActivity", "Fetched data: " + data);
} catch (IOException e) {
Log.e("MainActivity", "Failed to fetch URL: ", e);
}
return null;
}
}
}
The result of running the code of this application should appear in the logcat viewer of your Android Studio installation, and it should look like this:
I/MainActivity: Fetched data: {"geonames": [
{
"summary": "The Glärnisch is a mountain of the North-Eastern Swiss Alps, overlooking the valley of the Linth in the Swiss canton of Glarus. It consists of several summits of which the highest is 2,915 metres above sea level (...)",
"elevation": 2880,
"geoNameId": 2660595,
"feature": "mountain",
"lng": 8.998611,
"distance": "0.1869",
"countryCode": "CH",
"rank": 91,
"lang": "en",
"title": "Glärnisch",
"lat": 46.998611,
"wikipediaUrl": "en.wikipedia.org/wiki/Gl%C3%A4rnisch"
},
The benefit of using an AsyncTask
is that our main thread is now
completely free to keep receiving input and events, and our user will be
able to scroll, navigate and perform any other task while the network call
returns – or not.
The JSON result contains information about the points of interest in a canton of Switzerland called Glaris – a beautiful place you should definitely visit one day! If you do, do not forget to let the author know and hopefully we could meet in person and talk about Android watching the Swiss Alps. |
Permissions
If you have followed the instructions above, most probably the code did not work, and that is OK; we have forgotten to add the required permissions to our application.
By default, and for security reasons, Android applications are not allowed
to perform many operations, such as connecting to the internet, accessing
the list of contacts on your device or using the camera. You have to
manually give permission to perform each of these operations, and this is
done in the AndroidManifest.xml
file.
<uses-permission android:name="android.permission.INTERNET"/>
OkHttp
To finish this overview of simple HTTP connectivity, we are going to
perform the same operation as previously, but instead of using
HttpURLConnection
we are going to use an open source library called
OkHttp.
OkHttp is a wildly popular Android and Java networking library. It is referenced by many other libraries in the open source world around Android, and in many ways it can be considered the equivalent of AFNetworking.
Follow along
The code of this section is located in the |
Using it is extremely simple, and we are going to reuse the GeoNames API we have used previously. First you must add the dependency in Gradle, as shown in listing "Adding OkHttp as a dependency."
compile 'com.squareup.okhttp3:okhttp:3.4.2'
Next we are going to rewrite the APIConnector
class we created before, so
that it uses OkHttp instead. And the final code could not be simpler, as shown
in listing "Using OkHttp."
public class APIConnector {
public String getStringData(String urlString)
throws IOException {
URL url = new URL(urlString);
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
Response response = client.newCall(request).execute();
return response.body().string();
}
}
OkHttp offers the full spectrum of services expected of a networking library, and it is very simple to use.
5.3. Parsing JSON Data
The result of our call is a long string containing a certain amount of information codified in the venerable JSON format. This format has become a de facto standard for web APIs, and as such we are going to see how to convert this JSON code into native data structures that we can manipulate with Java. This process is called parsing and deserializing the JSON string.
Follow along
The code of this section is located in the |
We are going to create a new project now, using the default parameters as
usual, in we are going to reuse our APIConnector
class from our previous
project.
This time we are going to create a POJO (also known as "Plain Old Java
Object") that we will call PointOfInterest
– it will hold the
information returned by the JSON returned from the API call.
Quickly Creating Setters and Getters
Create a class called |
public class PointOfInterest {
private String summary;
private int elevation;
private int geoNameId;
private String feature;
private double lng;
private String distance;
private String countryCode;
private int rank;
private String lang;
private String title;
private double lat;
private String wikipediaUrl;
public PointOfInterest(JSONObject obj) throws JSONException {
summary = obj.getString("summary");
elevation = obj.getInt("elevation");
if (obj.has("geoNameId")) {
geoNameId = obj.getInt("geoNameId");
}
if (obj.has("feature")) {
feature = obj.getString("feature");
}
lng = obj.getDouble("lng");
distance = obj.getString("distance");
countryCode = obj.getString("countryCode");
rank = obj.getInt("rank");
lang = obj.getString("lang");
title = obj.getString("title");
lat = obj.getDouble("lat");
wikipediaUrl = obj.getString("wikipediaUrl");
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder(title);
// ...
Parsing JSON with Gson
We all agree that the code in the previous section is quite verbose, and if we have to do the same for every POJO in our application we are going to end up with a substantial amount of boilerplate code scattered throughout our application.
We can do better, because Google thankfully provides Gson, an open source library that removes the need for adding this code manually for every deserialized object in our application. Gson helps developers to serialize and deserialize JSON structures into native Java objects fast, easily and efficiently. It can handle large amounts of data and is very fast.
Equivalent of Gson in iOS
In the world of iOS apps, Mantle
plays more or less the same role than Gson. You can also think of Gson as an
equivalent of Cocoa’s |
First we need to add the dependency to our application-level Gradle file, as shown in Adding Gson as a dependency in the build.gradle file.
Follow along
The code of this section is located in the |
compile 'com.google.code.gson:gson:2.8.0'
Once this is done, we can modify the APIConnector
class as shown in
listing APIConnector class using Gson.
public List<PointOfInterest> getPointsOfInterest() throws IOException {
String json = getStringData();
JsonParser parser = new JsonParser();
JsonObject root = parser.parse(json).getAsJsonObject();
JsonArray geonames = root.get("geonames").getAsJsonArray();
Type collectionType = new TypeToken<List<PointOfInterest>>(){}.getType();
Gson gson = new Gson();
List<PointOfInterest> points = gson.fromJson(geonames, collectionType);
return points;
}
And that is all. Because our PointOfInterest
class has exactly the same
field names as the JSON returned by the API, we can use Gson here to
perform a very simple 1-to-1 mapping of fields and keys.
5.4. Parsing XML Data
The same web service we have used so far can return XML data; the only difference is that we have to remove the "JSON" word from the URL, and just by using the same parameters, we will have a simple XML output.
Follow along
The code of this section is located in the |
In this section we are going to learn how to use the standard "SAX-style" XML parser functionality built-in into Android.
About XML Parsers
There are two different kinds of XML parsers:
-
SAX-style
-
DOM-style
A SAX-style XML parser is event-based, and consume a stream of data; it is usually very fast, requires very little RAM, and is perfectly adapted for the low-power, battery-fed world of smartphones. On the other hand, it is usually hard to manipulate and code, particularly if the XML stream is complex.
On the other hand, a DOM-style XML parser loads a whole XML file in memory at once, and offers a tree-based approach with nodes and children nodes. This is usually a much simpler programming model, but it has increased memory requirements, which makes it suitable only for small amounts of XML data.
Using The Android SAX-style XML Parser
The XML data returned from the web service looks like the output in listing "XML data returned by the web service".
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<geonames>
<entry>
<lang>en</lang>
<title>Glärnisch</title>
<summary>The Glärnisch is a mountain… </summary>
<feature>mountain</feature>
<countryCode>CH</countryCode>
<elevation>2880</elevation>
<lat>46.9986</lat>
<lng>8.9986</lng>
<wikipediaUrl>http://en.wikipedia.org/wiki/Gl%C3%A4rnisch</wikipediaUrl>
<thumbnailImg/>
<rank>91</rank>
<distance>0.1869</distance>
</entry>
<!-- ... -->
</geonames>
The code required to parse this XML stream is located in the
training.akosma.xmlparsing.XmlPOIParser
class in the sample project. The
most important method of that class is reproduced in listing
"XML SAX parser", and shows how the parser advances through the XML
stream until there is no more data to process.
private List readFeed(XmlPullParser parser)
throws XmlPullParserException, IOException {
List points = new ArrayList();
parser.require(XmlPullParser.START_TAG, ns, "geonames");
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
// Starts by looking for the entry tag
if (name.equals("entry")) {
points.add(readPOI(parser));
} else {
skip(parser);
}
}
return points;
}
The code above references the readPOI()
method, which itself watches the
stream for specific tags, and builds the required PointOfInterest
instance accordingly, as shown in listing "XML SAX parser".
private PointOfInterest readPOI(XmlPullParser parser)
throws XmlPullParserException, IOException {
parser.require(XmlPullParser.START_TAG, ns, "entry");
PointOfInterest poi = new PointOfInterest();
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
if (name.equals("lang")) {
poi.setLang(readString(parser, "lang"));
} else if (name.equals("title")) {
poi.setTitle(readString(parser, "title"));
} else if (name.equals("summary")) {
poi.setSummary(readString(parser, "summary"));
} else if (name.equals("feature")) {
poi.setFeature(readString(parser, "feature"));
} else if (name.equals("countryCode")) {
poi.setCountryCode(readString(parser, "countryCode"));
} else if (name.equals("wikipediaUrl")) {
poi.setWikipediaUrl(readString(parser, "wikipediaUrl"));
} else if (name.equals("distance")) {
poi.setDistance(readString(parser, "distance"));
} else if (name.equals("elevation")) {
poi.setElevation(readInt(parser, "elevation"));
} else if (name.equals("geoNameId")) {
poi.setGeoNameId(readInt(parser, "geoNameId"));
} else if (name.equals("lat")) {
poi.setLat(readDouble(parser, "lat"));
} else if (name.equals("lng")) {
poi.setLng(readDouble(parser, "lng"));
} else if (name.equals("rank")) {
poi.setRank(readInt(parser, "rank"));
} else {
skip(parser);
}
}
return poi;
}
The whole process becomes relatively simple thanks to the structure of the XML data, which has no major complexities. Keeping track of the different tags in the XML stream can be problematic using this method. For example, if the same XML tag appear at different levels in the stream, then the developer is forced to keep track of the current "depth" level of the tree and the current tag name, in order to parse the data correctly. This can quickly become complex.
5.5. Displaying Data in Lists
One of the most common tasks that iOS and Android developers perform every
day consists in loading data from some backend network service and display
it in a list. This is so common that we are going to dedicate a complete
section to it, and we are going to discover just how similar it is to use
a RecyclerView
than to use a UITableView
instance.
Follow along
The code of this section is located in the |
This sample code consists in a RecyclerView
instance contained within
a PointOfInterestListFragment
object, itself contained within the
MainActivity
of our class. However, instead of loading the activity
through the XML layout – just like we did in chapter "User Interface",
this time we are going to load the fragment programmatically using the
FragmentManager
system.
This is a complementary mechanism to that of XML layouts, and allows for greater flexibility; fragments loaded using the Fragment Manager can be replaced, removed and changed at runtime, which is something that XML-based fragments cannot do.
To use the fragment manager is very simple, as shown in listing "Loading fragments using a FragmentManager".
public class MainActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.main_container);
if (fragment == null) {
fragment = new PointOfInterestListFragment();
fm.beginTransaction()
.add(R.id.main_container, fragment)
.commit();
}
}
}
The fragment itself contains an instance of
theandroid.support.v7.widget.RecyclerView
class, which as the
package name implies is part of the Support Libraries of Android. It has
been recently added to the platform, and it has quickly become the
de-facto mechanism to display lists in Android. It is very simple to use
and its API looks very similar to that of the UITableView
class in iOS.
The RecyclerView
class is not available by default in Android projects;
images "Project dependencies" and "Add the RecyclerView dependency in the project" show
how to add the required dependencies in the project when selecting
or using the Cmd+↓ (arrow down)
keyboard shortcut, and then selecting the "Dependencies" tab.
We need to give the RecyclerView
the data to be displayed; for that, we
have to create an Adapter
which is an object that extends the
RecyclerView.Adapter
class. An adapter is nothing else than the
"data source" of the recycler view; its role is to return a "Holder" view
(what we could simply call a "cell" in iOS) for each item in the list.
Listing "Adapter for the RecyclerView" shows the adapter for our
RecyclerView
.
private class PointOfInterestAdapter
extends RecyclerView.Adapter<PointOfInterestHolder> {
private List<PointOfInterest> mData;
public PointOfInterestAdapter(List<PointOfInterest> points) {
mData = points;
}
@Override
public PointOfInterestHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(getActivity());
View view = inflater.inflate(R.layout.list_item_poi, parent, false);
return new PointOfInterestHolder(view);
}
@Override
public void onBindViewHolder(PointOfInterestHolder holder, int position) {
PointOfInterest poi = mData.get(position);
holder.bind(poi);
}
@Override
public int getItemCount() {
return mData.size();
}
}
The onCreateViewHolder()
method of the adapter is the local equivalent
of the tableView:cellForRowAtIndexPath:
method in the
UITableViewDataSource
protocol in Cocoa. This method returns a subclass
of RecyclerView.ViewHolder
which is in many ways equivalent to the
UITableViewCell
class.
Generic Class
The |
The local subclass of RecyclerView.ViewHolder
is shown in listing
"ViewHolder subclass".
private class PointOfInterestHolder
extends RecyclerView.ViewHolder
implements View.OnClickListener {
private PointOfInterest poi;
private TextView mTitleTextView;
private TextView mLatitudeTextView;
private TextView mLongitudeTextView;
private TextView mSummaryTextView;
public PointOfInterestHolder(View v) {
super(v);
v.setOnClickListener(this);
mTitleTextView = (TextView) v.findViewById(R.id.name_text_view);
mLatitudeTextView = (TextView) v.findViewById(R.id.latitude_text_view);
mLongitudeTextView = (TextView) v.findViewById(R.id.longitude_text_view);
mSummaryTextView = (TextView) v.findViewById(R.id.summary_text_view);
}
public void bind(PointOfInterest poi) {
this.poi = poi;
mTitleTextView.setText(poi.getTitle());
mLatitudeTextView.setText(String.valueOf(poi.getLat()));
mLongitudeTextView.setText(String.valueOf(poi.getLng()));
mSummaryTextView.setText(poi.getSummary());
}
@Override
public void onClick(View view) {
Toast.makeText(getActivity(), poi.getSummary(), Toast.LENGTH_LONG).show();
}
}
Once the application runs, the list appears on screen as shown in figure "List implemented with the RecyclerView class".
5.6. Retrofit
Retrofit is a high-level, strongly typed REST API wrapper library for Java and Android. It is built on top of OkHttp and provides a complete abstraction over the entities being served over the API. It is quite simple to use it but it requires a bit of infrastructure to setup.
Follow along
The code of this section is located in the The application talks to a server application built with
Node.js available in the For testing purposes the author of this lines was using ngrok to tunnel the local Node.js server so that it could be reached from any testing device. |
First you need to add the dependency in the app/build.gradle
file, as
shown in listing "Adding Retrofit as a dependency".
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
Then you need to create the infrastructure required to map the REST endpoints to your Android application:
-
The
ServiceGenerator
class wraps the creation of the different client objects used to connect to the backend, as shown in listing "Retrofit ServiceGenerator class." -
The
UsersClient
interface maps local Java methods with remote REST API methods, including their HTTP verbs and other contextual information, shown in "UsersClient interface." -
A local mode class
User
represents the data being manipulated throught the REST interface, visible in "User class."
public class ServiceGenerator {
// Do not use "localhost" or "127.0.0.1" here!
// Those point to the emulator, not to the local machine!
public static final String API_BASE_URL = "http://3f02fbbf.ngrok.io";
private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
private static Retrofit.Builder builder =
new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create());
public static <S> S createService(Class<S> serviceClass) {
Retrofit retrofit = builder.client(httpClient.build()).build();
return retrofit.create(serviceClass);
}
}
public interface UsersClient {
@GET("/users")
Call<List<User>> getUsers();
@GET("/user/{id}")
Call<User> getUser(@Path("id") int userId);
@POST("/users")
Call<User> createUser(@Body User user);
@PUT("/user/{id}")
Call<Void> updateUser(@Path("id") int userId, @Body User user);
@DELETE("/user/{id}")
Call<Void> deleteUser(@Path("id") int userId);
}
public class User {
private String name;
private int age;
private String country;
public String getName() {
return name;
}
// ...
Once all of these elements are in place, we can start creating, editing and deleting users using a relatively high-level, strongly typed interface, as shown in listing "Using the Retrofit infrastructure in your code."
UsersClient client = ServiceGenerator.createService(UsersClient.class);
User newUser = new User();
newUser.setAge(10);
newUser.setCountry("Latveria");
newUser.setName("Doctor Doom");
Call<List<User>> getUsersCall = client.getUsers();
getUsersCall.enqueue(new Callback<List<User>>() {
@Override
public void onResponse(Call<List<User>> call, Response<List<User>> response) {
if (response.isSuccessful()) {
List<User> users = response.body();
Log.i("MainActivity", "User: " + users.toString());
} else {
Log.e("MainActivity", "Error calling the API");
}
}
@Override
public void onFailure(Call<List<User>> call, Throwable t) {
Log.d("Error", t.getMessage());
}
});
The code speaks by itself; if your network API follows closely the design guidelines of the REST specification, you could benefit greatly from using such a library. One of the biggest benefits it is that it removes all boilerplate code and it makes your application logic stand out at first sight.
5.7. Summary
The most important thing to keep in mind when writing networking code in
Android is, just as with iOS, to keep long running tasks off the main
thread. There are many mechanisms to do this, starting with the
AsyncTask
class, which provides a handy solution for short-lived
requests. Most networking libraries automatically take their work to
a background thread for you.
Android includes a JSON parsing library, but the open source library Gson by Google is wildly popular too; it provides automatic serialization and deserialization of objects to and from any class (or collection thereof.)
Finally, speaking about open source libraries, OkHttp and Retrofit are also popular choices to connect Android applications to backend services. The former can be thought of the Android equivalent of AFNetworking, while Retrofit is more similar to RestKit, at least in spirit.
6. Storage
In this chapter we are going to learn several different mechanisms used to store and retrieve information in Android applications. These include reading application resources, writing and reading files to the sandbox, and storing information in SQLite databases.
6.1. TL;DR
For those of you in a hurry, the table below summarizes the most important pieces of information in this chapter.
Android | iOS | |
---|---|---|
Local documents |
|
|
External storage |
|
n/a |
Bundled resource |
|
|
Downloading files |
|
|
Notifications |
|
|
Periodic tasks |
|
|
Preferences |
|
|
Sqlite wrapper |
SQLiteOpenHelper |
FMDB |
ORM |
OrmLite |
Core Data |
Realm |
Realm |
Realm |
6.2. Bundled Resources
The first and simplest way to store files in an Android application is by adding files to the application bundle. You can add any kind of file to your application in Android Studio, and these files will be added to the final APK file, and distributed with the binary.
This is very similar to what happens in iOS; in Xcode, developers can add
any kind of file to the application bundle, and it is precisely the
NSBundle
class that is used to retrieve those resources at runtime.
Follow along
The code of this section is located in the |
In the Resources
application, we have added an XML file, downloaded from
the
"findNearbyWikipedia"
GeoNames web service. This is exactly the data we were using in
Networking when connecting to the internet. The first lines of
the file appear in listing Embedded data resource file.
<geonames>
<entry>
<lang>en</lang>
<title>Glärnisch</title>
<summary>The Glärnisch is a mountain of the Glarus Alps, overlooking the valley of the Linth in the Swiss canton of Glarus. It consists of several summits of which the highest (2,914 metres) is the Bächistock, followed by the Vrenelisgärtli--"Verena's Little Garden"—at 2,904 metres) and the Ruchen (2,901 (...)</summary>
<feature>mountain</feature>
<countryCode>CH</countryCode>
<elevation>2880</elevation>
<lat>46.9986</lat>
<lng>8.9986</lng>
<wikipediaUrl>http://en.wikipedia.org/wiki/Gl%C3%A4rnisch</wikipediaUrl>
<thumbnailImg/>
<rank>85</rank>
<distance>0.1869</distance>
</entry>
The res/raw folder does not
exist by default in Android Studio projects. You have to right-click on
the res folder and select the option to create a new folder. After a few
seconds, the folder and its contents will be available through the R
class.
|
To add a file to the newly created
res/raw folder (or to any other folder in an Android Studio project, for
that matter) select the file in the Finder and hit the Cmd+C
keystroke. Select the target folder in Android Studio and hit Cmd+V keystroke. That is right! It is just a matter of copying and pasting
the file where you need it.
|
To load this information at runtime, just use the
getResources().openRawResource(R.raw.data)
method call, which returns an InputStream
object, as shown in
Programmatically loading a resource file.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
InputStream is = getResources().openRawResource(R.raw.data);
Document doc = builder.parse(is);
Element root = doc.getDocumentElement();
NodeList entries = root.getChildNodes();
for (int i = 0; i < entries.getLength(); ++i) {
Node entry = entries.item(i);
NodeList fields = entry.getChildNodes();
for (int j = 0; j < fields.getLength(); ++j) {
Node field = fields.item(j);
String name = field.getNodeName();
if (name.equals("title")) {
String title = field.getTextContent();
Log.i("MainActivity", "Title node found: " + title);
break;
}
}
}
} catch (SAXException e) {
Log.e("MainActivity", "Error parsing XML", e);
} catch (IOException e) {
Log.e("MainActivity", "Error reading file", e);
} catch (ParserConfigurationException e) {
Log.e("MainActivity", "XML parser configuration error", e);
}
}
}
By the way, as a complementary option, instead of using the
org.xmlpull.v1.XmlPullParser
class to perform a SAX-style parsing of the
XML, in this example we use the org.w3c.dom.Document
class, which
implements a DOM-style XML parser. The main difference between both
SAX and DOM parsers is that the DOM requires to load the contents of the
whole XML file in memory at once, which might only be useful for small XML
payloads.
6.3. Downloading Files
If your application requires to download very large files, you can do that in the background with a little bit of infrastructure. The downloads themselves will be handled completely by the operating system, which actually includes a very handy application (quite aptly named "Downloads") which shows the various file download tasks launched from different applications.
Follow along
The code of this section is located in the |
This is a similar system to that offered by background download tasks in iOS, with the added benefit of an ad-hoc application that allows you to manage and work with the downloaded file, as shown in Background tasks in iOS.
- (void)applicationDidEnterBackground:(UIApplication *)application
{
UIApplication *app = [UIApplication sharedApplication];
UIBackgroundTaskIdentifier task;
task = [app beginBackgroundTaskWithExpirationHandler:^{
[app endBackgroundTask:task];
}];
}
In Android, you can trigger a file download in the background using the code shown in Triggering a file download in the background, where we are downloading an image from Wikipedia.
final DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
File folder = new File(Environment.getExternalStorageDirectory() + "/AndroidTutorial");
if (!folder.exists()) {
folder.mkdirs();
}
String url = "https://upload.wikimedia.org/wikipedia/commons/b/b4/LocationItaly.png";
Uri uri = Uri.parse(url);
DownloadManager.Request request = new DownloadManager.Request(uri);
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI
| DownloadManager.Request.NETWORK_MOBILE)
.setAllowedOverRoaming(false)
.setTitle("AndroidTutorial")
.setDescription("Wikipedia image")
.setDestinationInExternalPublicDir("/AndroidTutorial", "photo.jpg");
mQueueID = manager.enqueue(request);
Of course, you might as well be interested in being notified when the file
is ready to be used; in this case you have to create an instance of
BroadcastReceiver
and verify that you have received the correct
notification, in this case ACTION_DOWNLOAD_COMPLETE
.
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0);
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(mQueueID);
Cursor c = manager.query(query);
if (c.moveToFirst()) {
int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS);
if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) {
ImageView view = (ImageView) findViewById(R.id.image_view);
String uriString = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
view.setImageURI(Uri.parse(uriString));
}
}
}
}
};
registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
You can think of a BroadcastReceiver as an object subscribing to
notifications sent by the NSNotificationCenter in Cocoa.
|
For this code to work, remember to add the proper permissions, as shown in Permissions required for file downloading.
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
The official Android documentation recommends asking for the
READ_EXTERNAL_STORAGE permission as well, as this will be required in
the future. Be sure to add it, to ensure your applications will work as
soon as this change is made.
|
6.4. Saving and Reading Files Locally
Sometimes the best option for your application is just to persist the data created by the user in a file stored locally. Android offers two different solutions for this:
-
Internal Storage, available to all applications in all Android devices, and sandboxed from other applications in the same device.
-
External Storage, usually in the form of SD cards, not available in all Android devices, and not sandboxed – which means that any application can read the data stored by any other application.
In this section we are going to see how to use Internal Storage, with
a very simple application that stores and reloads a text written on an
EditText
. A very simple application to keep just one note at all times!
The source code is very simple and it is shown in Writing and reading a text file.
Follow along
The code of this section is located in the |
private void writeFile() {
try {
File file = new File(getFilesDir(), filename);
FileOutputStream os = new FileOutputStream(file);
String text = editor.getText().toString();
os.write(text.getBytes());
os.close();
Log.i("MainActivity", "File saved");
} catch (Exception e) {
Log.e("MainActivity", "Problem writing file", e);
}
}
private void readFile() {
try {
File file = new File(getFilesDir(), filename);
if (file.exists()) {
FileInputStream is = new FileInputStream(file);
int content;
StringBuilder contents = new StringBuilder();
while ((content = is.read()) != -1) {
contents.append((char)content);
}
editor.setText(contents.toString());
is.close();
}
} catch (Exception e) {
Log.e("MainActivity", "Problem reading file", e);
}
}
Readers with previous experience with Java will instantly recognize that the APIs used by Android are strictly the same provided in the standard Java libraries.
We also want to call the writeFile()
and readFile()
methods when the
application goes and returns from the background, and not only that, but
we would like to automatically save our text every 5 seconds when the
application is active. Let us see how we can do that in
Automatic saving and loading.
private int mInterval = 5000;
private Handler mHandler = new Handler();
Runnable mPeriodicWriter = new Runnable() {
@Override
public void run() {
writeFile();
mHandler.postDelayed(mPeriodicWriter, mInterval);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
editor = (EditText) findViewById(R.id.editor);
}
@Override
protected void onPause() {
super.onPause();
writeFile();
mHandler.removeCallbacks(mPeriodicWriter);
}
@Override
protected void onResume() {
super.onResume();
readFile();
mPeriodicWriter.run();
}
As shown above, the android.os.Handler
class can be used, coupled to
a Runnable
instance, to trigger the execution of code periodically,
similarly to how the NSTask
class is used in Cocoa.
6.5. Storing User Preferences
Another common requirement of applications is to store small bits of information, usually booleans, numbers and strings, as preferences for the current user. These preferences can be used to customize the behavior and the look and feel of the application, to accomodate the… preferences of the current user.
Follow along
The code of this section is located in the |
As shown in Loading user preferences, the
android.preference.PreferenceManager
class is
used to access the SharedPreferences
object holding the preferences of
the user. This class is very similar to the NSUserDefaults
class in iOS,
and provides pretty much the same interface, allowing you to store atomic
elements of information that can be used to customize the application at
runtime.
SharedPreferences pm = PreferenceManager.getDefaultSharedPreferences(this);
String value = pm.getString("value", "default value");
boolean decision = pm.getBoolean("decision", true);
int age = pm.getInt("age", 30);
Android Studio also proposes to create activities to allow the user to
express those preferences from a graphical user interface. The
SettingsActivity
class in the Storage/Preferences
project shows how
to do that.
6.6. SQLite
Android contains a very simple wrapper class that allows developers to manipulate SQLite databases. These are actually the same databases that iOS and Core Data use, but the major difference is that Android does not include an object-relational mapper in its toolkit. In this sense, Android offers something more similar to FMDB, a popular open source SQLite wrapper for Objective-C and Swift applications.
In the next section we are going to see how to use a lightweight ORM in Android applications, but for the moment we are going to use SQLite as it is natively available.
Follow along
The code of this section is located in the |
The API of this wrapper contains two major classes:
android.database.sqlite.SQLiteDatabase
SQLiteDatabase represents the
SQLite file being accessed, and android.database.sqlite.SQLiteOpenHelper
SQLiteOpenHelper is the helper class that manipulates instances of the
former class. Applications must subclass SQLiteOpenHelper
and provide
the commands required to open, drop and modify tables, indexes and other
database components.
The listing "Subclassing SQLiteOpenHelper" shows how to subclass
SQLiteOpenHelper
in order to create the required tables inside of the file.
public class DatabaseHelper extends SQLiteOpenHelper {
public static final String FILE_NAME = "data.sqlite";
public static final int VERSION = 1;
public static final String PEOPLE_TABLE = "people";
public static final String ID_FIELD = "id";
public static final String NAME_FIELD = "name";
public static final String AGE_FIELD = "age";
public DatabaseHelper(Context context) {
super(context, FILE_NAME, null, VERSION);
}
@Override
public void onCreate(SQLiteDatabase database) {
StringBuilder sb = new StringBuilder("CREATE TABLE ");
sb.append(PEOPLE_TABLE)
.append(" (")
.append(ID_FIELD)
.append(" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL")
.append(", ")
.append(NAME_FIELD)
.append(" TEXT")
.append(", ")
.append(AGE_FIELD)
.append(" INT")
.append(");");
database.execSQL(sb.toString());
}
@Override
public void onUpgrade(SQLiteDatabase database, int i, int i1) {
}
}
As you can see, you have to properly format your SQL statements, following the official SQL syntax supported by SQLite.
SQL in SQLite
Every database vendor implements their own "dialect" of SQL, and SQLite is not the exception. Please take a look at the official SQL documentation for SQLite, and test your queries against a local database, for example using the command line SQLite client application. Android Studio will not prompt any warnings about wrong SQL queries, and your application will definitely fail at runtime if you have errors in them. |
Once you have your application-specific database helper, you just need to use
it in your application. In this case we are creating a small application to
store contact data, and we have created a small Person
class to help us model
the data.
Listing "Database Query" shows how to use the helper subclass in
your own application, performing a SELECT
statement.
private void queryValues() {
String table = PEOPLE_TABLE;
String[] columns = new String[]{ID_FIELD, NAME_FIELD, AGE_FIELD};
String selection = null;
String[] selectionArgs = null;
String groupBy = null;
String having = null;
String orderBy = NAME_FIELD;
Cursor peopleCursor = mDatabase.query(table,
columns,
selection,
selectionArgs,
groupBy,
having,
orderBy);
List<Person> people = new ArrayList<>();
Person p;
peopleCursor.moveToFirst();
if (!peopleCursor.isAfterLast()) {
do {
long id = peopleCursor.getLong(0);
String name = peopleCursor.getString(1);
int age = peopleCursor.getInt(2);
p = new Person();
p.setId(id);
p.setName(name);
p.setAge(age);
people.add(p);
} while (peopleCursor.moveToNext());
}
peopleCursor.close();
Log.i("MainActivity", "Found people: " + people);
}
Whenever you query your database using the helper, you will be served an
instance of android.database.Cursor
, a helper object which
allows you to "move forward" in the list of records that match your
criteria. The do … while
statement in the middle of the queryValues()
method does precisely that, and creates a Person
instance at each
iteration.
To insert values in the database, we are going to use another helper
class, the android.content.ContentValues
class. The
Person
class shows how to create instances of ContentValues
, which are
basically key-value maps.
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
values.put(NAME_FIELD, name);
values.put(AGE_FIELD, age);
return values;
}
The only remaining step consists in giving that ContentValues
bag to the
DatabaseHelper
and let it do its job, inserting the data in the database.
private void insertValues() {
Person p1 = new Person();
p1.setName("John Smith");
p1.setAge(56);
Person p2 = new Person();
p2.setName("Maria Rogers");
p2.setAge(33);
ContentValues values1 = p1.getContentValues();
long id1 = mDatabase.insert(PEOPLE_TABLE, null, values1);
ContentValues values2 = p2.getContentValues();
long id2 = mDatabase.insert(PEOPLE_TABLE, null, values2);
}
The SQLiteOpenHelper
class offers much more functionality, all of which is
described in extensive detail in the Android documentation.
6.7. OrmLite
OrmLite offers a lightweight and strongly-typed alternative to the use of plain SQLite statements in your application. It is a simple ORM that targeted primarily the Java world, and was then adapted to the Android platform, where it works beautifully well.
In this section we are going to rewrite our contact management application, but this time using OrmLite to manage the storage of our entities.
Follow along
The code of this section is located in the |
At the moment of this writing, developers must integrate OrmLite manually in their project by downloading the Jar files and adding them in Android Studio. It is not as convenient, but it is not very difficult to do either.
First make sure to download the latest binaries of OrmLite in the official download page; at the time of this writing, it is version 5.0.
Download OrmLite
When you download OrmLite, make sure to download both the
|
Then create a new Android Studio project, and in the Project pane, select the
"Project" perspective. Inside that view, you will see the tree of your project;
open up the branches of the tree until you see the app/libs
folder. It turns
out that Android Studio (thanks to Gradle) automatically compiles anything that
you drop in the app/libs
folder, so you can just drag and drop your Jar files
inside that folder. A couple of seconds later the symbols contained in those
archives should be available to your code.
The only remaining thing then is to write the application. We are once again
going to create a helper class, but this time we are going to subclass the
com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper
class. Listing
"OrmLite Database Helper" shows how the final version of the file should look like.
public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
private static final String DATABASE_NAME = "helloAndroid.db";
private static final int DATABASE_VERSION = 1;
private Dao<Person, Integer> personDao = null;
private RuntimeExceptionDao<Person, Integer> personRuntimeDao = null;
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION, R.raw.ormlite_config);
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase, ConnectionSource connectionSource) {
try {
Log.i(DatabaseHelper.class.getName(), "onCreate");
TableUtils.createTable(connectionSource, Person.class);
} catch (SQLException e) {
Log.e(DatabaseHelper.class.getName(), "Can't create database", e);
throw new RuntimeException(e);
}
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, ConnectionSource connectionSource, int i, int i1) {
try {
Log.i(DatabaseHelper.class.getName(), "onUpgrade");
TableUtils.dropTable(connectionSource, Person.class, true);
// after we drop the old databases, we create the new ones
onCreate(sqLiteDatabase, connectionSource);
} catch (SQLException e) {
Log.e(DatabaseHelper.class.getName(), "Can't drop databases", e);
throw new RuntimeException(e);
}
}
public Dao<Person, Integer> getDao() throws SQLException {
if (personDao == null) {
personDao = getDao(Person.class);
}
return personDao;
}
public RuntimeExceptionDao<Person, Integer> getPersonDao() {
if (personRuntimeDao == null) {
personRuntimeDao = getRuntimeExceptionDao(Person.class);
}
return personRuntimeDao;
}
@Override
public void close() {
super.close();
personDao = null;
personRuntimeDao = null;
}
}
OrmLite works by configuration, using standard Java attributes. We are going to
"decorate" our Person
class with those, so that OrmLite knows what to do with
each piece of information. This is shown in listing "Person class with attributes."
@DatabaseTable(tableName = "people")
public class Person {
@DatabaseField(generatedId = true)
private long id;
@DatabaseField(index = true)
private String name;
@DatabaseField
private int age;
// ...
The final piece of the puzzle is a configuration file, which is expected
to be located in the res/raw
folder. This file contains more information
about the database to be created by OrmLite.
#
# generated on 2012/06/06 05:55:57
#
# --table-start--
dataClass=training.akosma.ormlite.Person
tableName=people
# --table-fields-start--
# --field-start--
fieldName=id
generatedId=true
# --field-end--
# --field-start--
fieldName=name
indexName=person_string_idx
# --field-end--
# --field-start--
fieldName=age
# --field-end--
# --table-fields-end--
# --table-end--
#################################
Indeed, the syntax of this file is quite strange.
Once this is all done and ready, the only remaining piece of the puzzle is to
actually use the Person
instances in your code; listing
"Using OrmLite in your application" shows how to do that.
RuntimeExceptionDao<Person, Integer> dao = getHelper().getPersonDao();
Person john = new Person();
john.setName("John Smith");
john.setAge(35);
dao.create(john);
Person lucy = new Person();
lucy.setName("Lucy Skies");
lucy.setAge(42);
dao.create(lucy);
List<Person> people = dao.queryForAll();
for (Person person : people) {
Log.i("MainActivity", "Found person: " + person);
}
OrmLite provides annotations for many other parameters, all of which is specified in the documentation pages in the official website.
6.8. Realm
Realm is a relative newcomer in the storage arena, but it is the first solution of its kind to have been thought with Post-PC devices (smartphones and tablets) in mind from the very beginning.
It is extremely simple to use, it is fast, it has a ridiculously low memory footprint, it is compatible with iOS and Android, it has APIs for Swift, Objective-C, Java and C#, and needless to say, it is wildly popular among Android developers as well.
We are going to implement our contact management application now using Realm, and as you will see, this will be a very short section indeed.
Follow along
The code of this section is located in the |
The first thing you need to do is, as expected, to add a line in the root
build.gradle
file. This will automatically download the required libraries
and will make them available to your code.
classpath "io.realm:realm-gradle-plugin:2.2.0"
Use the root build.gradle file!
In the case of Realm, we must add the dependency directly at the level of the module, not of the application itself. |
Once this is done, we must subclass our model classes from the
io.realm.RealmObject
class. This is the base class for all model objects
that are stored at runtime in a Realm database.
public class Person extends RealmObject {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(name);
sb.append(" (")
.append(age)
.append(")");
return sb.toString();
}
}
Nothing mysterious here; just define the fields that you want, the getters and setters that you need, and you are good to go.
Finally, to use Realm, just execute transactions and add, edit and remove your objects in those.
Realm.init(this);
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().build();
Realm.setDefaultConfiguration(realmConfiguration);
realm = Realm.getDefaultInstance();
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
Person john = realm.createObject(Person.class);
john.setName("John Smith");
john.setAge(40);
Person mary = realm.createObject(Person.class);
mary.setName("Mary Poppins");
mary.setAge(60);
}
});
Person person = realm.where(Person.class).equalTo("age", 60).findFirst();
Log.i("MainActivity", "Person found: " + person);
And that is it! Your data will be automatically saved in your device as you run your code. Realm even provides a free browser application for macOS (available through the Mac App Store) which allows developers to inspect the internal structure of their applications at any given time.
6.9. Summary
Android offers a large array of options to store and retrieve information in a variety of persistent mediums. Whether as part of a bundle, as a downloaded file from the Internet, as a file stored in the internal or external storage, or as structured data in a database, there are quite a few ways to make sure your users will not forget anything.
The most common mechanism for Android apps to store structured data is SQLite, but this requires manually managing the schema of the information stored in the file. Android includes a SQLite wrapper which provides exactly this functionality.
If you need a bit more flexibility, you can opt for OrmLite, an Object Relational Mapper built for Java that works perfectly well on Android devices. And of course there is Realm, a modern, fast option for structured on-device storage, offering compatibility with iOS devices as well, and a free viewer application for macOS.
Part 4: Sensors and Multimedia
Android devices feature an incredible range of sensors and hardware, and the operating system has very simple yet powerful APIs to consume their data. This part will dive into the interaction with many different sensors and how to exploit their multimedia capabilities.
7. Sensors
Android devices come bundled with a large array of useful sensors, just like iOS devices. Actually, some Android devices even carry more sensors than have ever been available on iPhones and iPads, like temperature or humidity sensors. This chapter will give you a quick overview of the most important sensors available, and how to access and consume the data they produce.
Run in Device!
The code samples of this chapter must be run in a real device! Although the Android Emulator is capable of emulating sensors as well, it is very important to test your code in a real device. |
7.1. TL;DR
For those of you in a hurry, the table below summarizes the most important pieces of information in this chapter.
Android | iOS | |
---|---|---|
Framework |
|
Core Motion & Core Location |
Main class |
|
|
Callback methods |
|
Blocks |
Sensor data |
|
|
Location |
|
|
7.2. Getting a List of Sensors
Android devices are not all built equal. Some of them are bundled with a plethora of sensors, and some of them – usually the more affordable ones – include just a basic subset.
iOS developers are used to dealing with a much more stable hardware platform, and charts like James Dempsey’s iOS Device Summary or the iOS Support Matrix show how stable is the world we have to deal with.
In the case of Android, hardware diversity is the rule; in 2015 Google acknowledged more than 4000 different kinds of devices running Android, built by more than 400 different manufacturers and supported by 500 different mobile carriers all over the world.
This means that Android developers must absolutely test the existence of any sensors they intend to use in their devices, before actually using them.[2]
Follow along
The code of this section is located in the |
To get a list of the available sensors in your own device, you can
create a simple application with a ListView
and populate it using the
code shown in Obtaining a list of sensors in your Android device.
SensorManager manager; (1)
manager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
List<Sensor> sensor = manager.getSensorList(Sensor.TYPE_ALL);
int layout = android.R.layout.simple_list_item_1;
ListView lv = (ListView) findViewById(R.id.view_list);
ListAdapter adapter = new ArrayAdapter<Sensor>(this, layout, sensor);
lv.setAdapter(adapter);
1 | The android.hardware.SensorManager class plays a similar role in
Android to that of CMMotionManager in iOS. |
Running this code in an actual Android device yields the screen shown in image "Sensors available in a OnePlus 3 Android Device".[3]
The table Available Android Sensors shows in detail the different sensors available in the Android operating system.
Name | Sensor Identifier | Type | Unit | Available in Emulator |
---|---|---|---|---|
Linear Acceleration |
|
Software or Hardware |
m/s2 |
|
Accelerometer |
|
Hardware |
m/s2 |
Yes |
Air Pressure |
|
Hardware |
hPa or mbar |
Yes |
Gravity |
|
Software or Hardware |
m/s2 |
|
Gyroscope |
|
Hardware |
rad/s |
|
Humidity |
|
Hardware |
% |
Yes |
Light |
|
Hardware |
lx |
Yes |
Magnetometer |
|
Hardware |
μT |
Yes |
Orientation |
|
Software |
rad |
|
Proximity |
|
Hardware |
cm |
Yes |
Rotation |
|
Software or Hardware |
||
Temperature |
|
Hardware |
°C (Celsius) |
Yes |
As you can see, some of them are totally unheard of in iOS! Please note that not all sensors are available in all models of Android devices; always check for the availability of a sensor before attempting to consume its data!
Taking Screenshots in Android
To take a screenshot from your Android device programmatically and then to
transfer it to your computer, just run the following adb devices -l This will show a list of available Android devices. You will need the ID of your target device if you have several devices connected or emulators running at the same time! Create the screenshot as follows: adb -s xxxxxxxx shell /system/bin/screencap -p /sdcard/screenshot.png The adb -s xxxxxxxx pull /sdcard/screenshot.png screenshot.png |
7.3. Using the Accelerometer
Longtime iOS developers will remember a time when we had to use the
UIAccelerometer
in the UIKit framework to access the data coming from
the accelerometer… Since iOS 4, however, the functionality was moved
into the Core Motion framework, where it has been available ever since.
The SensorManager Class
In Android we use the android.hardware.SensorManager
class as the entry point for accessing data from the accelerometer. As you
might expect, the usual workflow consists in creating a manager object,
and setting another object as the "listener" (or "delegate" in iOS speak)
of the manager object. Once the connection is done, the listener will
periodically receive the relevant data, to manipulate it as needed.
Follow along
The code of this section is located in the |
In the case of Android, listeners must implement the
SensorEventListener
interface, which defines the required methods
expected by the sensor manager. Of these, the most important method is
onSensorChanged()
which is called periodically with a parameter of type
SensorEvent
. This object contains a .values
array with a variable
number of float
values; from one (in the case of the light sensor) to
three (in the case of movement sensors).
API Uniformity
No matter which sensor you use in Android, the delegate interface and the
|
Listing "Consuming accelerometer data" shows how to set a simple activity as the listener for the accelerometer events.
public class MainActivity extends AppCompatActivity
implements SensorEventListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
(1)
mManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
mAccel = mManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
@Override
public void onSensorChanged(SensorEvent sensorEvent) { (2)
float[] values = sensorEvent.values.clone();
// values = lowPass(values);
float xValue = values[0];
float yValue = values[1];
float zValue = values[2];
x.setText(String.format("%1.1f", xValue));
y.setText(String.format("%1.1f", yValue));
z.setText(String.format("%1.1f", zValue));
double vector = Math.sqrt(xValue * xValue +
yValue * yValue +
zValue * zValue);
mVector.setText(String.format("%1.1f", vector));
if (vector < 3) {
root.setBackgroundColor(Color.RED);
} else if (vector < 10) {
root.setBackgroundColor(Color.GREEN);
} else if (vector < 40) {
root.setBackgroundColor(Color.YELLOW);
} else {
root.setBackgroundColor(Color.RED);
}
}
@Override
protected void onResume() { (3)
super.onResume();
mManager.registerListener(this,
mAccel,
SensorManager.SENSOR_DELAY_NORMAL);
}
@Override
protected void onPause() { (4)
super.onPause();
mManager.unregisterListener(this);
}
protected float[] lowPass(float[] input) { (5)
float[] acceleration = {0, 0, 0};
final float FACTOR = 0.1f;
for (int i = 0; i < 3; ++i) {
acceleration[i] = input[i] * (1.0f - FACTOR);
}
return acceleration;
}
}
1 | Here we request a SensorManager instance to the operating system,
and then we use that object to obtain a reference to the accelerometer. |
2 | This method is called every time that the accelerometer has new data. |
3 | This activity registers itself as a listener when it becomes active… |
4 | …and of course, being a good citizen, it deregisters itself before going to the background. |
5 | A small filter method, to reduce the noise in the signal retrieved from the accelerometer. |
The application is built in such a way that the screen becomes yellow when you shake your device a little bit, red when you shake it a lot (or when you let it fall freely!) and green when it stays quiet.
Disclaimer!
Pay attention not to break your device! The usual disclaimer applies here: I am not responsible of any destruction caused by this sample or its use. |
The execution of this code is shown in figure "Consuming accelerometer data."
7.4. Using the Compass
Accessing data from the compass (or, using a technically more correct term, the magnetometer requires using the exactly same API as we have seen in the previous section.
We are going to jump directly to the code required to make a nice compass out of that information, which actually requires as well the use of the accelerometer information. Listing Consuming information from the magnetometer shows how to use the information to finally be able to set the rotation of the image that represents the compass needle.
Follow along
The code of this section is located in the |
@Override
public void onSensorChanged(SensorEvent sensorEvent) {
if (sensorEvent.sensor == mAccelerometer) {
mLastAccData = sensorEvent.values.clone();
mIsAccDataReady = true;
} else if (sensorEvent.sensor == mMagnetometer) {
mLastMagnData = sensorEvent.values.clone();
mIsMagnDataReady = true;
}
if (mIsAccDataReady && mIsMagnDataReady) {
float[] rotationMatrix = new float[9];
SensorManager.getRotationMatrix(rotationMatrix,
null,
mLastAccData,
mLastMagnData);
float[] orientation = new float[3];
SensorManager.getOrientation(rotationMatrix, orientation);
float radians = orientation[0];
float degrees = (float) Math.toDegrees(radians);
float rotation = (float) (degrees + 360) % 360;
mCompassView.setRotation(-rotation);
}
}
In the case of the compass, to be able to read properly the current inclination
of the device compared to the magnetic axis of Earth we need to take into
account the information from the accelerometer as well. We pass this
information to the SensorManager.getRotationMatrix()
method, and later we
pass that result to SensorManager.getOrientation()
method, which ultimately
provides the rotation information that we need to apply to the animated object
on the screen.
7.5. Location Information
The release of the iPhone 3G with a GPS chip, and then the massive
adoption of Android, have made location-bound applications extremely
popular on all mobile app stores. The reason for the proliferation of
location-enabled applications is most probably due to the ease of access
to location information from the devices. We know that in iOS, the
CLLocationManager
class in the Core Location framework provides all the
information required.
In the case of Android, the situation is a bit more complicated, since
there are several ways to access this information at this moment in time.
Historically, location information was available through the
android.location.LocationManager
class, but lately the preferred method
is through the com.google.android.gms.location.LocationServices
class. This last class is available through the
Play Services library, which is available in all Android devices including
Google’s own Play Store – that is, most of them.
We are going to learn in this section how to use the information provided by this library and how to consume location information properly.
Follow along
The code of this section is located in the |
First things first; as you might imagine, we need to modify the app Gradle file with some new libraries.
compile 'com.google.android.gms:play-services-location:9.8.0'
Once this is ready, the symbols of this library will be available throughout
the project for us to use. But then again, because consuming location
information represents a potential privacy problem, we have to request the
permissions in the AndroidManifest.xml
file.
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>
We are going to make our MainActivity
class a listener for location
events; we are just going to implement the
GoogleApiClient.ConnectionCallbacks
and
GoogleApiClient.OnConnectionFailedListener
interfaces.
public class MainActivity extends AppCompatActivity implements
GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
Then we need to create an instance of the GoogleApiClient
class. Not only
that, but we are also going to make the current activity connect and disconnect
only when the activity becomes visible; this will save battery power on the
device and will make a better use of the resources.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mLatitude = (TextView) findViewById(R.id.view_latitude);
mLongitude = (TextView) findViewById(R.id.view_longitude);
mClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(LocationServices.API)
.build();
}
@Override
protected void onStart() {
super.onStart();
mClient.connect();
}
@Override
protected void onStop() {
super.onStop();
mClient.disconnect();
}
As soon as the GoogleApiClient
instance is connected to the Google backend
systems, our application will be notified and we should, since Android 6, ask
the user for permissions once again. This is a new recommended practice, and it
only involves a couple more lines of code.
@Override
public void onConnected(@Nullable Bundle bundle) {
final String fine = Manifest.permission.ACCESS_FINE_LOCATION;
final String coarse = Manifest.permission.ACCESS_COARSE_LOCATION;
if (!hasPermission(fine)
&& !hasPermission(coarse)) {
String[] permissions = new String[]{
fine,
coarse
};
ActivityCompat.requestPermissions(this, permissions, 0);
return;
}
retrieveLocation();
}
private boolean hasPermission(String permission) {
int check = ActivityCompat.checkSelfPermission(this, permission);
return check == PackageManager.PERMISSION_GRANTED;
}
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 0) {
boolean granted = true;
for (int i = 0; i < grantResults.length; i++) {
granted = granted && (grantResults[i] == PERMISSION_GRANTED);
}
if (granted) {
retrieveLocation();
}
}
}
After all of this back and forth, we are finally ready to process some location
information. The LocationServices.FusedLocationApi.requestLocationUpdates()
takes a callback object as a parameter, and inside of it we are going to
inspect the values we are interested in, like in this case, the latitude and
the longitude of the current user.
private void retrieveLocation() {
try {
LocationRequest request = LocationRequest.create();
request.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
request.setNumUpdates(1);
request.setInterval(0);
LocationServices.FusedLocationApi.requestLocationUpdates(mClient,
request,
new LocationListener() {
@Override
public void onLocationChanged(Location location) {
double lat = location.getLatitude();
double lng = location.getLongitude();
mLatitude.setText(String.valueOf(lat));
mLongitude.setText(String.valueOf(lng));
}
});
} catch (SecurityException e) {
Log.e("MainActivity", "Error: ", e);
}
}
The Play Store API is not as straightforward as the one offered by
CLLocationManager
, but the end result is the same.
7.6. Retrieving Address Information
Using the code of the previous section as a basis, we are going to extend it and ask Google for the current address of our location in Planet Earth. This will involve a few more callback methods, and a new concept: Intent Services, used to perform long-running operations in a separate thread.
Follow along
The code of this section is located in the |
The bulk of the application is pretty much the same as the one from the previous section; same permissions, same callbacks, same Google Play Services client.
protected void startIntentService() {
Intent intent = new Intent(this, AddressService.class);
intent.putExtra(Constants.RECEIVER, mResultReceiver);
intent.putExtra(Constants.LOCATION_DATA_EXTRA, mLastLocation);
startService(intent);
}
This will instantiate and launch a new Intent Service of the AddressService
class. Intent Services are used in Android as an alternative to AsyncTask
instances, and are more appropriate for some long-running tasks.
Inside of the AddressService
we unpack the latitude and longitude passed by
the caller activity, and we proceed to call the geocoder service.
Geocoder geocoder = new Geocoder(this, Locale.getDefault());
addresses = geocoder.getFromLocation(
location.getLatitude(),
location.getLongitude(),
1);
Once the AddressService
instance has received the required information, it
passes it back to the calling activity as follows.
private void deliverResultToReceiver(int resultCode, String message) {
Bundle bundle = new Bundle();
bundle.putString(Constants.RESULT_DATA_KEY, message);
mReceiver.send(resultCode, bundle);
}
This information is made available to the activity through a subclass of
the android.support.v4.os.ResultReceiver
class, in
our case the AddressResultReceiver
class.
class AddressResultReceiver extends ResultReceiver {
public AddressResultReceiver(Handler handler) {
super(handler);
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
// Display the address string
// or an error message sent from the intent service.
String address = resultData.getString(Constants.RESULT_DATA_KEY);
mAddress.setText(address);
}
}
7.7. Summary
Android devices are bundled with lots of different sensors, but thankfully the API used to access most of them is centralized in a single class, which provides uniformity and makes consuming their data much simpler. It is very important to query the current device for the list of available sensors, since not all Android devices are bundled with the same hardware, and then, only if the sensor is available, your code should proceed.
Android provides a native class for accessing location information, but most applications use the one provided in the Google Play Services library, which is available in all the devices that include the Google Play Store. This library is updated independently of the Android operating system, throught the Play Store application, a fact which seemingly updates older devices to new functionality in spite of the slow upgrade rate of newer versions of the operating system.
Finally, the new permission request system in place since Android 6 means that applications must ask for permission at runtime, before accessing critical information.
8. Multimedia
Android devices are fully fledged multimedia stations. People use them to connect to their friends in social media, posting pictures in Instagram, sharing videos on YouTube and even recording and publishing podcasts. This chapter will provide a short introduction to the most important APIs available to developers for managing image, audio and video data.
8.1. TL;DR
For those of you in a hurry, the table below summarizes the most important pieces of information in this chapter.
Android | iOS | |
---|---|---|
Display image |
|
|
Display video |
|
|
Viewer application |
Gallery |
Photos |
Image picker |
|
|
Audio recorder |
|
|
Audio player |
|
|
Text to speech engine |
|
|
8.2. Taking Pictures
Arguably, the simplest and most common thing we all do with our smartphones, Android or not, is to take pictures. Smartphone cameras have become so popular that some camera vendors had to adapt their strategies for this new world, and new business models and startups have appeared and flourished around those little cameras in our pockets.
So let us start our discussion by learning how to retrieve an image from the Android camera. Turns out, it is as simple as doing it on iOS!
Follow along
The code of this section is located in the |
The first thing you have to do in your project is to add the proper permissions, as usual. In this case we are going to notify the operating system that this application requires a camera; this has the effect of filtering out from the results in the Play Store in case a device does not include a camera (are there smartphones without camera these days?)
<uses-feature android:name="android.hardware.camera"
android:required="true" />
Once this is done, you can request the camera activity from within your
code just by creating an Intent
instance with the proper parameters,
as shown in listing "Requesting the camera with an Intent."
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, CODE);
}
Once the user has taken the picture, your main activity can simply
retrieve the image from the intent using the "data"
key (in this case,
shown as the KEY
constant in the code)
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CODE && resultCode == RESULT_OK) {
Bundle extras = data.getExtras();
Bitmap imageBitmap = (Bitmap) extras.get(KEY);
ImageView picture = (ImageView) findViewById(R.id.view_picture);
picture.setImageBitmap(imageBitmap);
}
}
The CODE
constant is an integer, used to distinguish the intent from
other data requests performed in the same activity.
Gallery Application
As you might expect, images and videos taken by the user are automatically saved and visible in the "Gallery" application, just like in the "Photos" application in iOS. |
8.3. Recording Video
As you will see, using the camera to record video is not much more
complicated. The difference is the type of Intent
, and the fact that
instead of retrieving the whole data generated by the operation, we are
going to receive a Uri
from the intent, which we will feed to an
instance of the android.widget.VideoView
class.
Follow along
The code of this section is located in the |
The permissions we need to add in our AndroidManifest.xml
is the
same shown in listing "Adding permissions in the AndroidManifest.xml file." Once this is done,
just create the Intent and start it, as shown in "Requesting the camera for videos."
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, CODE);
}
Once the user has taken the picture, your main activity can simply
retrieve the image from the intent using the "data"
key (in this case,
shown as the KEY
constant in the code)
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
if (requestCode == CODE && resultCode == RESULT_OK) {
Uri videoUri = intent.getData();
mVideoView.setVideoURI(videoUri);
mVideoView.start();
}
}
To be able to control the playback of the video, please remember to always
add an instance of android.widget.MediaController
to your activity;
otherwise the user will not be able to launch, pause or stop the playback
of the video.
mController = new MediaController(this);
mVideoView = (VideoView) findViewById(R.id.view_video);
mVideoView.setMediaController(mController);
VideoView and Layouts
The VideoView class, when inside a |
8.4. Picking Images
Asking the user to select a picture from his local gallery is another very simple task, which as you might imagine requires asking for permissions and to create an intent.
You might have noticed, however, that in some cases it is enough to ask for the permission on the AndroidManifest.xml file, while in other cases we have had to ask for permission in our code as well. Why is this?
The reason for this is that the permission model in Android API 23+ has changed, and there are now two types of permissions:
-
Normal permissions, which have very little privacy risk (as assessed by Google) such as accessing the internet, the Bluetooth stack, setting alarms or vibrating. These can be requested safely in the AndroidManifest.xml file without further due, and the operating system grants them automatically upon installation.[4]
-
Dangerous permissions, on the other hand, can raise serious privacy issues, and as such since API 23 developers must explicitly ask for them at runtime. Examples of these permissions include those to access the calendar, the contacts, the microphone, the location or the telephony subsystem.[5]
In our code we are going to need to access the photo library, which requires a "dangerous" permission, so we will need to add the code required to request that permission.
Follow along
The code of this section is located in the |
We are going to modify our AndroidManifest.xml
file to include the
required permissions, a compatibility measure with devices running older
versions of Android, as shown in listing "Adding permissions in the AndroidManifest.xml file."
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
Then we need to add the required permission request in our activity, as shown in listing "Requesting permissions at runtime."
int permission = ActivityCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (permission != PackageManager.PERMISSION_GRANTED) {
// We don't have permission so prompt the user
ActivityCompat.requestPermissions(
this,
PERMISSIONS_STORAGE,
REQUEST_EXTERNAL_STORAGE
);
}
We can then proceed and ask the system to show the image picker for the current user; but these days there are many different sources for images. The current user might have a collection of pictures in a Dropbox folder that she might want to use, so why not giving them the chance to use them?
Listing "Showing an image picker with multiple sources" shows how to create a chooser intent, which will display a very handy menu to your user, allowing her or him to pick an image from a large variety of sources, as shown in image "Image picker with multiple sources." Choosing "Android System" will display the standard "Photos" or "Gallery" applications, while the "Files" option will bring the "Files" application, allowing the user to select an image from Dropbox, Google Drive, a folder in the local device or any other location available.
Intent getIntent = new Intent(Intent.ACTION_GET_CONTENT);
getIntent.setType("image/*");
Intent picker = new Intent(Intent.ACTION_PICK,
android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
picker.setType("image/*");
Intent[] intents = new Intent[]{picker};
Intent chooser = Intent.createChooser(getIntent, "Select Image");
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents);
startActivityForResult(chooser, CODE);
Once the user chooses an image, the activity can retrieve its Uri
and
display it in the application, as shown in listing "Retrieving the Uri to the picture."
@Override
protected void onActivityResult(int requestCode,
int resultCode,
Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CODE && resultCode == RESULT_OK) {
Uri uri = data.getData();
ImageView image = (ImageView) findViewById(R.id.view_picture);
image.setImageURI(uri);
}
}
8.5. Recording and playing audio
Android devices usually feature a microphone, most commonly associated for voice calls, and speakers, generally used for music playback. But nothing – apart from pesky but necessary permissions – prevent developers to access them and to arbitrarily record audio. This section will show how simple it is to record and play audio on an Android application.
Follow along
The code of this section is located in the |
As usual, we are going to add the required permissions to our
AndroidManifest.xml
file, as shown in listing
"Requesting Permissions in the AndroidManifest.xml file."
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
Of course, we have to deal with permissions at runtime as well since API
23, so let us do that in the onStart()
method as shown in listing
"Requesting permissions at runtime."
final String write = Manifest.permission.WRITE_EXTERNAL_STORAGE;
final String record = Manifest.permission.RECORD_AUDIO;
if (!hasPermission(write) || !hasPermission(record)) {
String[] permissions = new String[]{
write,
record
};
setState(ApplicationState.FORBIDDEN);
ActivityCompat.requestPermissions(this, permissions, 0);
return;
}
setState(ApplicationState.IDLE);
By the way, the hasPermission()
method is a little helper, shown in
listing "The code of the hasPermission() helper method."
private boolean hasPermission(String permission) {
int check = ActivityCompat.checkSelfPermission(this, permission);
return check == PackageManager.PERMISSION_GRANTED;
}
We have to check the response from the user, of course; if the permissions were granted, then proceed as expected. Otherwise, just disable the user interface, because there is not much to do, as shown in listing "The callback from the operating system granting permissions – or not."
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
setState(ApplicationState.IDLE);
} else {
setState(ApplicationState.FORBIDDEN);
Toast.makeText(this,
"Unfortunately, cannot do much without permissions...",
Toast.LENGTH_LONG).show();
}
}
If the user has granted us the permissions, the next step would be to
record some sounds; for that, the android.media.MediaRecorder
is
straightforward to setup and launch, as shown in listing
"Setting up a recorder object."
mRecorder = new MediaRecorder();
mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mRecorder.setOutputFile(mFileName);
mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
mRecorder.prepare();
setState(ApplicationState.RECORDING);
mRecorder.start();
The recorder is set to write an MP3 file directly on the file system; if
this file exists, the PLAY will be enabled automatically, and we can
launch the playback. For this we use an instance of
android.media.MediaPlayer
, and off we go, as shown in listing
"Setting up a player object."
mPlayer = new MediaPlayer();
mPlayer.setDataSource(mFileName);
mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
stop();
}
});
mPlayer.prepare();
setState(ApplicationState.PLAYING);
mPlayer.start();
Similarities with iOS
As the attentive reader might have discovered by now, the
|
The player stops automatically when the sound file reaches its end, but we
can always hit the STOP button and cancel all operations. In this
case we have to release our player (or our recorder) and set those
references to null
, to ensure that the garbage collector will take care
of those old objects for us, as shown in listing "Stopping playback."
private void stop() {
if (ApplicationState.RECORDING == mState && mRecorder != null) {
mRecorder.stop();
mRecorder.release();
mRecorder = null;
} else if (ApplicationState.PLAYING == mState && mPlayer != null) {
mPlayer.stop();
mPlayer.release();
mPlayer = null;
}
setState(ApplicationState.IDLE);
}
8.6. Speech Synthesizer
To close this chapter, we are going to learn a very simple API that drives
the built-in text-to-speech system of the Android operating system,
available since Lollipop (API 21). This API is very similar to that of the
AVSpeechSynthesizer
class in iOS, as you will see.
Follow along
The code of this section is located in the |
Remember that this API is available only since API 21 (Lollipop, Android
5.0) so we are going to modify our app/build.gradle
file to indicate
this requirement, as shown in listing "Build Gradle file with minimum API requirement."
minSdkVersion 21
Using the API involves using the android.speech.tts.TextToSpeech
class, as shown in listing "Using the TextToSpeech class."
public class MainActivity extends AppCompatActivity
implements TextToSpeech.OnInitListener {
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final EditText field = (EditText) findViewById(R.id.edit_text);
final TextToSpeech tts = new TextToSpeech(this, this);
tts.setLanguage(Locale.US);
mButton = (Button) findViewById(R.id.button_speak);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
tts.speak(field.getText(), TextToSpeech.QUEUE_ADD, null, "test");
}
});
Button clearButton = (Button)findViewById(R.id.button_clear);
clearButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
field.setText("");
}
});
}
@Override
public void onInit(int i) {
mButton.setEnabled(true);
}
}
After running the application on a device, the user can type any text in
English in the TextView
shown on the screen, and the device will speak
that text back.
8.7. Summary
Android devices offer a large array of possibilities when it comes to image, audio and video. Using the proper permissions and intents, applications can prompt users to take and select images, videos, and to record and play audio files.
However interesting these capabilities are, they raise important privacy
concerns, and since API 23 developers are expected to request permissions
to access these features at runtime; this means that the classical
permissions in AndroidManifest.xml
are no longer enough. There is an
important distinction between "normal" and "dangerous" permissions; the
former are granted automatically by the operating system upon
installation, while the latter must be requested explicitly in newer
versions of the operating system.
There is also a very easy to use text-to-speech system built in the operating system, which opens the door to many interesting applications.
Part 5: High Quality Apps
Mobile applications used to be small projects. As time passed and as their usefulness grew, Android applications became incredibly big enterprises, involving tens of developers and spanning through tens or hundreds of lines of code. This part will delve into some useful techniques to ensure that your codebases stay flexible, maintainable and that your team retains its sanity and happiness.
9. Architecture
Mobile applications are only small in terms of screen size. As they become popular and increase their feature set, their code can become more and more complex, and the teams building them are at risk of increasing their technical debt every day.
This book includes a whole section about code quality, but it is important to have a discussion on code quality through this chapter about architecture. Right now you know how to create applications, with activities and fragments, and you also feel confident about adding networking and storage code; it is now time to organise that code for evolution.
9.1. TL;DR
For those of you in a hurry, the table below summarizes the most important pieces of information in this chapter.
Android | iOS | |
---|---|---|
Notification center |
|
|
Flexible designs |
Interface-oriented programming |
Protocol-oriented programming |
Dependency injection |
Dagger |
Typhoon – Swinject – Cleanse … |
ReactiveX |
RxJava & RxAndroid |
RxSwift |
Observing data |
|
|
9.2. Principles of Good Android Architecture
This section will introduce some techniques of Android application architecture that will make your code more manageable and easier to support.
Organise your UI around Fragments
Organising your UI code in fragments is by far the most important tip you can keep in mind while you build your user interface. By design, Android activities are what Apple used to call "screenfuls of information," which presents problems when you port your applications to larger screens, such as tablets or even large phones.
Activities should only host fragments and never contain any "business logic"; in many cases, for example when creating layouts for smartphones, they will only hold one big fragment that takes the whole screen. In the case of tablets, there might be many fragments sharing the screen space at any given time.
By using fragments, you will have the freedom to organise your user experience in different ways, and this fact will bring an additional benefit to your code: modularity, separation of concerns, and easier maintainability.
Save Activity State
One of the most radically different features of Android compared to iOS is the fact that activities are destroyed and recreated when users change the orientation of their devices. This is something that puzzles iOS developers to no end when they start writing applications for this platform.
However, as surprising and annoying as it is, there is a benefit hiding beneath the hood. Just like in iOS, the Android operating system reserves its right to destroy any application and its associated activities at any given time, depending on the current memory requirements of ths system.
By adding the required state management code to your activities, you will actually be able to recover gracefully from low memory conditions, in which your application might be forced to quit. As soon as the user reselects your activity, you will be able to restore the current application state gracefully, and this is something that your user will be thankful for.
Listen to Memory Warnings
The memory management scheme in Android devices is not very different from that of iOS; in both cases, the operating system keeps an eye on the overall memory consumption of all applications, giving some privilege to the one that is currently in the foreground. If the memory requirements of any application or service becomes critical, the operating system reverse its right to start removing applications from memory, but before this happens, it will signal all applications with "memory warnings" (does it ring a bell?)
All applications should listen to these memory warnings at all time, and behave properly by releasing memory when required. The same heuristics apply to Android as to iOS:
-
Release early and often those objects you will not need anymore.
-
Whatever can be cached, should be cached.
-
Whatever can be recalculated, should be recalculated.
-
Remember that lazy loading is your friend, and never allocate more than you really need.
Please refer to "Out of memory when using graphics" in Chapter "Graphics" of this book, which contains an extensive explanation of common low-memory situations in Android devices (hint, it is not very different from iOS) and some strategies to solve them, as well as the APIs involved.
Organise your Code in Packages
Android Studio makes it really easy to organise your code in Java packages, reflecting logical groups of classes, enums or interfaces. Isolating the code components in separate groups could help you in the future, for example to reuse particular components in another application.
"Protocol Oriented Programming" in Java
Arguably, one of the most popular new concepts brought by Swift to iOS and Cocoa is that of "Protocol Oriented Programming", a set of techniques to ensure that your code is flexible, adaptive and manageable.
However excited as Swift developers could be, this idea is, historically speaking, hardly new. Scott Meyers was telling C++ developers to use "Abstract Base Classes" (ABCs) back in the 90s. [Bloch] also mentioned the importance of "coding against interfaces" in Java in his book.
Whatever name you give to this technique, it always boils down to the
following rule: always try to use the most abstract possible type for
your variables and objects. The canonical example could be the following
snippet, where the variable list
is typed as a java.util.List
– that
is, a generic interface shared by all Java ordered sequences, such as
java.util.Vector
, java.util.Stack
, java.util.LinkedList
and many
others.
List<String> list = new ArrayList<>();
By following this simple principle, you will ensure your architecture is flexible enough to be used in other contexts, with other concrete types, but always with testability and type-checking in mind.
Protocol or Interface Extensions
Please keep in mind that one of the basic principles of protocol-oriented programming in Swift, namely protocol extensions, is currently not available in the version of Java used by Android. However, this same mechanism is available in Kotlin, so if you look for a more modern language that supports the latest paradigms and fashion, you might as well want to take a look at it. |
9.3. Dagger
If you have been following closely the previous section, you will know by now that Java interfaces are the key element for code flexibility. Using interfaces, you can replace objects of one implementation with others featuring a different one; the only requirement being that both objects share the same interface.
But let us take this concept a bit further down the road. Let us say that your hobby is electronics and one weekend you decice that you want to build a calculator. You go to the hardware store, you buy an LED display, a couple of labeled plastic keys, some logic components and you wire all of them on the backyard. Of course, all of these components are heavily standardized, and as along as they support a similar voltage and that their connectors are standard, you could swap one capacitor for another, maybe cheaper or easier to find in your area.
This idea of composing complex objects out of simpler ones is a very powerful one, and using it in software yields incredibly good results. By decomposing your "high-level," most complex objects into other simpler ones, you can achieve many good things:
-
You can test each component separately, just like you can test each capacitor separately to make sure that they work in your electronics project.
-
You can swap one component by another easily, just by removing the old one and putting the new one in its place.
The capability of replacing a component with another sharing the same interface lead practicioners and computer scientists to invent the concept of "Dependency Injection." Using DI, an object can specify the concrete types to be used at runtime, instead of specifying them "by hand" or "hard coded" during development. The configuration of the object – that is, the concrete types to be used at runtime – can be specified in a separate object, or even on a configuration file outside of the project.
Follow along
The code of this section is located in the |
In this section we are going to learn a bit more about Dependency Injection using Dagger, a project now maintained by Google (originally created by developers from Square) which allows applications to be composed at runtime, as we explained previously.
To demonstrate how to use Dagger, we are going to build… precisely a calculator!
An Obsession with Calculators
If you have read my article on Medium called Being a Developer at 40 maybe you remember that I mentioned that, every time I learn a new programming language, I build a calculator with it. Well, this time is no exception! We are going to learn about Dependency Injection and Dagger by building one. |
First we need to integrate Dagger in our project. To do that, as usual, we are going to edit the Gradle file of our project:
compile 'com.google.dagger:dagger:2.7'
annotationProcessor 'com.google.dagger:dagger-compiler:2.7'
Android Studio will now download and compile the symbols of Dagger into our application.
Let us now analyze the structure of our calculator project in Android
Studio. The most important thing to know about it is that it uses Java
interfaces extensively; there is a Calculator
interface, which itself
requires instances of the Digit
interface to be entered, and uses an
Operation
(yet another interface) to calculate the final result. All of
these elements are grouped into their own packages, too.
If you take a look at the MainActivity
class, you will see that it does
absolutely nothing; its layout file, on the other hand, references
CalculatorFragment
– because we are following good practices here!
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/fragment"
android:name="training.akosma.rxcalculator.CalculatorFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
The interesting part comes next; our CalculatorFragment
has two public
fields marked with the @Inject
attribute, as shown in listing
"Inject attributes in CalculatorFragment."
public class CalculatorFragment extends Fragment {
@Inject
public Calculator mCalculator;
@Inject
public Storage mStorage;
These annotations are required by Dagger to know where to "inject" the
concrete implementations of the Calculator
and Storage
interfaces. But
where do we specify those concrete implementations?
This information is located in the CalculatorModule
class, a Dagger
@Module
which @Provides
the required implementations. Of course, this
could enjoy yet another level of abstraction, using a configuration file
or some other dynamic feature, but for the moment let us just use simple
new
statements to return the concrete objects.
@Module
public class CalculatorModule {
@Provides
static Calculator provideCalculator() {
return new LongIntegerCalculator();
}
@Provides
static Storage provideStorage() {
return new MemoryStorage();
}
}
So now we have a CalculatorFragment
class which requires some objects to
be @Inject
-ed, and we have a CalculatorModule
which @Provides
those
objects. We need to add some glue between both! This glue is the
CalculatorComponent
interface:
@Component(modules = {CalculatorModule.class})
public interface CalculatorComponent {
void inject(CalculatorFragment fragment);
}
When Dagger reads this file at compile time, it will automatically create
a hidden class called DaggerCalculatorComponent
using the module
specified as a parameter. This will happen transparently, without any user
intervention, and finally the only thing that remains to be done is to use
that generated code.
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// mCalculator = new IntegerCalculator();
// mStorage = new MemoryStorage();
mComponent = DaggerCalculatorComponent.builder().build();
mComponent.inject(this);
mCalculator.setStorage(mStorage);
}
As you can see above, we ask Dagger to build a component of type
CalculatorComponent
and then we ask that component to inject()
the
current fragment with objects generated by the CalculatorModule
class.
As a bonus, I have left commented out the code that performs exactly the
same task, but using new
statements hardcoded in the fragment class.
This is arguably a very complex setup for such a simple construction, but imagine if you were not building just a calculator, but a vehicle, a machine or any other construction with lots of moving parts. Imagine now being able to specify the interfaces of all those component types, and then let Dagger assemble the whole structure for your dynamically at application startup.
This is the power of Dependency Injection, and this is a simple example of it.
9.4. RxAndroid
There is a classic article in the web about software architecture, referenced once and again, its diagrams copied and rearranged a thousand time, one that the author of this book felt compelled to include as a reference, called The Clean Architecture, by "Uncle Bob", Robert C. Martin.
The architectural diagram included in that article, reproduced below (with a link to its original) departs from the classic vertical design to a more organic structure, in which outer layers can have knowledge and access to the inner ones, but not the other way around.
More about Clean Architecture
If you are interested in the concept of Clean Architecture and would like to learn more, please check this video with Robert C. Martin himself explaining the concept. |
Follow along
The code of this section is located in the |
In order to make our calculator project more "clean" we would like to avoid two things:
-
Whenever the user presses a button in the calculator, we have to update the display reflecting the current state of the calculator object. We would like to avoid having to poll this object, and instead we would love to be notified of those state changes.
-
The calculator should be complete unaware of the fact that it lives its life inside of an Android application fragment, and that its current state will be displayed on a
TextView
instance.
iOS developers reading this can imagine lots of different mechanisms to
reach this kind of simultaneous isolation and collaboration:
NSNotification
, "Key-Value Observing", delegate protocols, etc. We could
simply implement a delegate protocol in Java using interfaces, for
example; but in this case we are going to go a bit overboard, and we will
use RxAndroid instead.
RxAndroid is a port to Android of RxJava, the Reactive Extensions for the JVM. They allow developers to create asynchronous, event-based applications with observables.
To include RxAndroid in our project, as usual, we need to modify the application’s Gradle file:
compile 'io.reactivex:rxandroid:1.2.1'
compile 'io.reactivex:rxjava:1.1.6'
With this library available, we are going to make our
LongIntegerCalculator
fully conform to the Calculator
interface and
expose an observable to the outer world, as shown in listing
"Publish an observable."
private final PublishSubject<Long> mSubject = PublishSubject.create();
private Operation<Long> mCurrentOperation;
private long mTempRegister;
private long mRegister;
private long mDigitsCount = 0;
private Storage mStorage;
@Override
public Observable<Long> getObservable() {
return mSubject;
}
The mechanism enabled by the code above is eerily similar to that provided
by KVO in Cocoa, or by the property observers provided by Swift,
didSet
& willSet
.
Now the LongIntegerCalculator
instance can call
mSubject.onNext(mRegister)
every time that its internal register value
changes, as shown in listing "Notify observers of a change."
@Override
public void enterDigit(Digit digit) {
if (mDigitsCount == 0) {
mRegister = digit.getValue();
} else {
mRegister = mRegister * 10 + digit.getValue();
}
mDigitsCount += 1;
mSubject.onNext(mRegister);
}
@Override
public void enterOperation(Operation op) {
mTempRegister = mRegister;
mRegister = 0;
mCurrentOperation = op;
mDigitsCount = 0;
mSubject.onNext(mRegister);
}
But of course, what is an observable without an observer? Let us add the
required code in the CalculatorFragment
class to make sure that the
display of the calculator follows the evolution of the internals of the
calculator, as shown in listing "Subscribe to an observable."
mCalculator.getObservable().subscribe(new Observer<Long>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(Long value) {
mDisplay.setText(value.toString());
}
});
And now, every time that the user interacts with the calculator interface
in the CalculatorFragment
, the underlying Calculator
implementation will change its behavior, and the TextView
used as LED
display will be notified of that change, as expected.
9.5. Summary
Good architecture goes a long way, and this is not specific of Java. Most good practices applicable to all software apply to Android, and following them will spare you headaches and team conflicts in the future.
Dagger is a popular dependency injection library that you can use in your projects, to help your team compose objects at runtime with testable components.
Finally, RxAndroid is a recent addition to the ReactiveX family, bringing the power of Reactive Extensions for the JVM to the Android world. It will help you create asynchronous, event-based architectures with fewer dependencies among your components, helping you test and deploy better applications.
10. Testing
Quality is not a property of things, but rather of the process used to create things. In the realm of software, quality can be described as the process by which one makes the right software, while making the software right.
Few software development techniques have had more impact in the past quarter of a century than the spread of automated unit and functional testing. Fortunately for us, Android Studio incorporates very advanced testing features, including the capability of running some "pure Java, no UI" tests in the IDE, running other tests in a device or an emulator, and finally to simulate complete user interactions using functional tests. In this chapter we are going to learn about these capabilities.
10.1. TL;DR
For those of you in a hurry, the table below summarizes the most important pieces of information in this chapter.
Android | iOS | |
---|---|---|
Unit testing framework |
JUnit 4 |
XCTest |
Mock objects framework |
Mockito |
OCMock – OCMockito |
Most common issue |
|
(ObjC) Messages to nil & (Swift) optionals |
10.2. Defensive Programming Techniques
Chapter 8 of the famous "Code Complete, Second Edition" book by Steve McConnell is titled "Defensive Programming", and starts like this:
In defensive programming, the main idea is that if a routine is passed bad data, it won’t be hurt, even if the bad data is another routine’s fault. More generally, it’s the recognition that programs will have problems and modifications, and that a smart programmer will develop code accordingly.
Code Complete, Second Edition
Defensive programming techniques include: input checking, assertions, error handling techniques – including exceptions, debugging aids and barricades. In this section we are going to learn how to apply some of those techniques in Android projects.
Exceptions
Java has a well-defined exception handling mechanism, but it takes some time to learn to use it effectively, particularly because it is somewhat different to the error mechanisms available in Objective-C and in particular to those from Swift. Its misuse can cause trouble to both users and other team members.
Types of Exceptions
Java has two categories of exceptions:
-
Checked.
-
Unchecked.
The difference between both is that code advertising checked exceptions in the method signature must be wrapped in try / catch
/ finally
statements by developers; Swift actually uses a similar
feature, and this makes the compiler whine to the developers that do not
wrap such code around do / try
statements.
On the other hand, unchecked exceptions represents errors which are
not recoverable, like the dreaded java.lang.OutOfMemoryError
, usually
referred to as OOME
in the Java and Android literature.
Exception Hierarchy
The diagram "Java Exceptions Hierarchy" shows the hierarchy of exception classes in Java. All errors and exceptions inherit from Throwable
;
errors are by definition unchecked, as instances of RuntimeException
.
Developers are expected to recover from checked exceptions safely, shown
with a gray background in the diagram.
Exceptions & Errors between Android and Cocoa
Please be aware of this curious fact: Java and Objective-C use similar
names for two opposite concepts; |
Exception Handling Guidelines
To avoid having code sprinkled with a myriad of try / catch / finally
statements, it is recommended to follow the following best practices
:
-
Catch exceptions as close to the user as possible.
-
Code that is meant for reuse (libraries or shared code among multiple applications) should not try to do error handling. It can, however, translate technology-specific exceptions (usually checked) into unchecked, generic ones; as a canonical example, API code could wrap a
FileNotFoundException
(checked) into aRuntimeException
that would ultimately be thrown. -
Always report exceptions, and report only once. Do not leave empty
catch
blocks in your code, and do not rethrow after doing something with the exception (like logging). Particularly in Android, exceptional situations might be interesting for the end user, and should be reported. -
Prefer toasts for unimportant information, and only use dialogs for important notifications that require the attention of the user.
NullPointerException
One word of warning against one of the most common sources of crash in the
history of software, one which Java is particularly well known for: the
NullPointerException
. This is a subclass of RuntimeException
, that
is, an unchecked exception thrown when a method is invoked on a null
reference.
I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
Null References: The Billion Dollar Mistake
iOS developers, both coming from Objective-C and Swift, are particularly blessed in this respect; to begin with, Objetive-C has no problem sending messages to nil pointers anyway, which means that Cocoa applications written in this language will never crash because of this problem. Of course, this situation raises other problems, and to solve them Swift included the notion of optionals, which make explicit the possibility of a reference being nil, forcing the developer to "unwrap" it if needed.
Java 1.7 has none of these capabilities (actually, the latest version of Java, version 8 includes an Optional class, although its API is not quite the same as Swift’s.) Therefore, in the current state of things, always check for the existence of references before calling methods on them, particularly for those references returned by methods outside of your code, on which you have no control.
The techniques to avoid the NullPointerException
range from the most
simple if (obj != null)
to techniques such as the
Null Object Pattern;
whichever your choice, remember that you must take action against these
exceptions, as Java 1.7 will not help you.
A common NullPointerException source
Instead of |
Assertions
Use the assert
keyword in your programs to test for particular
situations during development, and run production code with assertions
turned off. This is a particularly good technique to prevent
NullPointerException
from appearing in production!
public int divide(int a, int b) throws IllegalArgumentException {
assert b != 0 : "The second parameter should not be zero!";
if (b == 0) {
this.setLastResult(0);
throw new IllegalArgumentException("Argument 'b' is 0");
}
this.setLastResult(a / b);
return this.getLastResult();
}
Even with assertions off, the code documents the fact that b
is not
expected to be zero in this context.
Assertions and Design by Contract
Assertions allow Java developers to specify pre- and postconditions in their code, following a technique formalized and pushed forward by the Eiffel programming language, created by Bertrand Meyer, called "Design by Contract™."
There is often confusion in programmers between assertions vs exceptions; when should they use either one? The answer is to remember that assertions should protect your code from your own errors, while exceptions are used to protect your code from errors coming from external libraries.
Of course, extensive unit testing – which, in itself, is based on the assertion principle – is a key element to increase your confidence in your shipped code.
10.3. The Monkey
The Android Monkey is a program that runs on your emulator or device generating pseudo-random streams of user events such as taps or gestures, as well as some system-level events. The idea of the Monkey is to stress-test applications under development, hoping that you will get a crash before your users do.
The Monkey is a command-line tool that that you can run on any emulator instance or on a device. It sends a pseudo-random stream of user events into the system, which acts as a stress test on the application software you are developing. The monkey can be launched from the command line, on an emulator or a device, and watching it using your application can be really fun!
adb shell monkey -p training.akosma.rxcalculator --throttle 100 -s 43686 -v 50000 | tee monkey.log
The parameters of adb shell monkey
command are the following:
-
-p
specifies the package name to test. -
--throttle
specifies the delay in milliseconds between the events. -
-s
specifies a seed value for the random number generator. This value should be changed every so often, to generate different interactions on the application UI. -
-v
specifies the verbose option. -
50000
is the number of events to be simulated.
The Monkey requires an instance of the Emulator running, with the
application specified in the -p
parameter installed in it. The emulator
can be run from the command line using the script below:
The output of the Monkey tool looks like this (edited for brevity):
… :Sending Touch (ACTION_DOWN): 0:(310.0,195.0) :Sending Touch (ACTION_UP): 0:(398.46033,164.18097) :Sending Trackball (ACTION_MOVE): 0:(-1.0,2.0) :Sending Touch (ACTION_DOWN): 0:(785.0,685.0) :Sending Touch (ACTION_UP): 0:(800.0,763.3364) :Sending Trackball (ACTION_MOVE): 0:(-1.0,-3.0) :Sending Trackball (ACTION_MOVE): 0:(-1.0,-2.0) //[calendar_time:2013-01-29 14:08:36.853 system_uptime:106295] // Sending event #100 :Sending Touch (ACTION_DOWN): 0:(274.0,1077.0) :Sending Touch (ACTION_UP): 0:(270.2971,1078.4357) :Sending Touch (ACTION_DOWN): 0:(28.0,84.0) :Sending Touch (ACTION_UP): 0:(11.232578,80.12936) :Sending Touch (ACTION_DOWN): 0:(547.0,1189.0) :Sending Touch (ACTION_UP): 0:(629.37836,1216.0) :Sending Trackball (ACTION_MOVE): 0:(-3.0,-5.0) :Sending Touch (ACTION_DOWN): 0:(209.0,113.0) …
The log file will also include crashes or anomalies found during the execution of the application.
Monkey sometimes causes problems with the adb server. If needed, use the
following commands to restart the adb server: |
10.4. Local Unit Testing
Let us jump to the most interesting subject of unit testing. Historically, unit testing in Android represented quite a challenge, one that iOS developers sometimes struggled to understand. Because Android does not run on the standard JVM, it meant that unit tests involving classes of the Android SDK could only run on the emulator. This made testing applications a bit difficult given the slow speed of the first emulators available.
Although this situation persists to this day – Android code can only run on the Android Runtime – thankfully Android Studio allows code involving only Java libraries to be unit tested directly on the IDE. These tests are called local unit tests and are extremely simple to write and run without running an emulator instance.
Follow along
The code of this chapter is located in the |
Local unit tests look exactly like you would expect from a modern unit
testing suite. The biggest difference with XCTest, however, strives in the
fact that test methods do not begin with the test
name, but are rather
decorated with the @Test
attribute.
Listing "Local unit test" shows an admittedly simple local unit test for our RxCalculator project.
public class AdditionTest {
@Test
public void execute() throws Exception {
Operation op = new Addition<Float>();
Float op1 = new Float(2);
Float op2 = new Float(3);
Number result = op.execute(op1, op2);
assertEquals(result.floatValue(), 5.0f, 0.001f);
}
@Test
public void getString() throws Exception {
Operation op = new Addition<Float>();
Float op1 = new Float(2);
Float op2 = new Float(3);
String value = op.getString(op1, op2);
assertEquals(value, "2.0 + 3.0 = 5.0");
}
}
If you would like to run these tests from the command line, just type the
./gradlew test
command and Gradle will gladly run them for you. This can
be particularly useful for configuring Continuous Integration (CI)
systems.
10.5. Instrumented Unit Testing
In contrast with local unit tests, Android Studio allows you to create instrumented unit tests, which run on an emulator or a device, and have full access to the complete Android SDK.
Robolectric
For those remembering the times of the Eclipse Android Developer Tools, instrumented unit tests were something one could achieve using a tool like Robolectric. |
Listing "Instrumented unit test" shows an instrumented unit tests, decorated
with the required @RunWith(AndroidJUnit4.class)
attribute.
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
@Rule
public ActivityTestRule<MainActivity> mActivityTestRule = new ActivityTestRule<>(MainActivity.class);
@Test
public void onCreate() throws Exception {
FrameLayout view = (FrameLayout) mActivityTestRule.getActivity().findViewById(R.id.activity_main);
assertEquals("android.widget.FrameLayout", view.getClass().getName());
TextView text = (TextView) view.findViewById(R.id.text_view_display);
assertNotNull(text);
}
}
10.6. User Interface Testing
Instrumented unit tests open the door to automated functional testing directly from within the Android Studio IDE using the Espresso library. The first step to use this feature consists in making sure that you have the required libraries in your application Gradle build file, as shown in listing "Including the testing libraries in build.gradle."
androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
testCompile 'junit:junit:4.12'
Of course, creating functional tests by hand is quite a complex task, and thankfully, once again, Android Studio comes to the rescue. Select the
runs the application and records the sequence of steps performed in an instrumented test.Listing "A recorded interaction with the calculator" shows the final result of simulating an interaction with the calculator, one that can be repeated ad aeternum et ad nauseam by your continuous integration server.
ViewInteraction appCompatButton9 = onView(
allOf(withId(R.id.button_8), withText("8"), isDisplayed()));
appCompatButton9.perform(click());
ViewInteraction appCompatButton10 = onView(
allOf(withId(R.id.button_delete), withText("DEL"), isDisplayed()));
appCompatButton10.perform(click());
ViewInteraction textView = onView(
allOf(withId(R.id.text_view_display), withText("7"),
childAtPosition(
childAtPosition(
withId(R.id.activity_main),
0),
0),
isDisplayed()));
textView.check(matches(withText("7")));
10.7. Code Coverage in Android Studio
The logical question after including a suite of unit tests in your project is, "how much of my code is covered by the tests?" Thankfully, once again Android Studio has you… covered.
You can easily configure the project to include the code coverage information, which is then compiled into a set of handy HTML files, ready for anyone in the team to inspect and refer to. To do that, select the Android Studio test run configuration."
menu, and create a JUnit Run configuration to run all local tests, using the parameters shown in figure "Once you have done this, you can select the "Run all local tests" configuration in the pop-up menu in the toolbar of Android Studio, and launch the code coverage operation clicking on the "Run with Coverage" button, highlighted in figure "Running unit tests with coverage." This figure also shows the coverage pane on the right hand side of the screen, as well as the Run pane at the bottom with the results of the last unit test run.
You can export the code coverage from the dedicated pane on Android Studio, which creates HTML files and optionally opens the default browser to display those results, as shown in figure "HTML report with code coverage."
Follow along
The coverage report of this chapter is located in the
|
If you want to generate the code coverage report from the command line,
just execute ./gradlew createDebugCoverageReport
.
10.8. Miscellaneous Tips
This section presents a series of simple tips and tricks that can be useful to increase the quality of your Android code.
StrictMode
StrictMode is a developer tool which detects things you might be doing by accident and brings them to your attention so you can fix them.
StrictMode is most commonly used to catch accidental disk or network access on the application’s main thread, where UI operations are received and animations take place. Keeping disk and network operations off the main thread makes for much smoother, more responsive applications. By keeping your application’s main thread responsive, you also prevent ANR dialogs from being shown to users.
To enable StrictMode, extend your project adding a custom subclass of the
android.app.Application
class:
package com.akosma.calculator;
import android.app.Application;
import android.os.Build;
import android.os.StrictMode;
public class CalcApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (Build.VERSION.SDK_INT >= 9 && isDebug()) { (1)
StrictMode.enableDefaults();
// StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
// .detectDiskReads().detectDiskWrites().detectNetwork()
// .penaltyLog().build());
// StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
// .detectLeakedSqlLiteObjects().detectLeakedClosableObjects()
// .penaltyLog().penaltyDeath().build()); (2)
}
}
private boolean isDebug() {
boolean isDebug = ("google_sdk".equals(Build.PRODUCT))
|| ("sdk".equals(Build.PRODUCT));
return isDebug;
}
}
1 | We only set the StrictMode when executing our application in the emulator;
this code should not be executed in production applications. |
2 | You can also call "detectAll()" for detecting all problems. |
Give Threads a Name
Give every thread a meaningful name. This includes thread pool threads. It makes stack dumps much more meaningful. It takes a little more effort to give a meaningful name to even thread pool threads, but if one thread pool has a problem in a long running application, the developer can cause a stack dump to occur, grab the logs, and without having to interrupt a running system you can tell which threads are deadlocked, leaking, growing, etc.
Immutable Objects
The Java API has no concept of immutable objects. The final
modifier
can be used in this case. For example, if a getter returns a List
object, make its getter return an immutable view on it, which blocks
client code from inadvertently modifying it, using the
Collections.unmodifiableList()
method:
public List<T> getList() {
return Collections.unmodifiableList(list);
}
Use final
on local variables to make them constants. Immutability is
always your friend, particularly in multi-threaded code, loops or complex
user interfaces.
If your getters return objects, return a copy, not the actual object, to make sure that it will not be mutated by the calling code.
Finally, use copy
constructors instead of clone()
.
Performance Tips
The Android documentation features several useful application performance tips that are worth enumerating here:
-
Avoid Creating Unnecessary Objects
-
Prefer Static Over Virtual
-
Use Static Final For Constants
-
Avoid Internal Getters/Setters
-
Use Enhanced For Loop Syntax
-
Consider Package Instead of Private Access with Private Inner Classes
-
Avoid Using Floating-Point
-
Know and Use the Libraries
-
Use Native Methods Carefully
-
Know And Use The Libraries
-
Use Native Methods Judiciously
Other Quality Tips
Some other useful tips for testing and debugging your apps.[6]
-
If you need to unit test code using SQLite databases, you might want to create that database in memory; for that, remember that
SQLiteOpenHelper()
creates an in-memory database when the second parameter,name
, isnull
. -
Writing a good
hashCode
implementation is really hard; you should instead just overrideequals()
instead. -
Please take a look at the Core App Quality checklists in the Android documentation before you ship your applications. These checklists range from visuals to functionality, and they can help you deliver quality software in time and budget, whatever your project.
10.9. Summary
Quality is much more than just adding tests. Quality starts by the recognition of the multiple sources of errors that exist, beginning with our own human limitations.
It is fundamental, then, to code defensively, using assert
to protect
your program from your own errors, and to catch Exceptions to protect it
from errors coming from the outside world.
Remember that Exceptions and Errors in Java have opposite meanings than in
Cocoa; Java Errors and unchecked Exceptions are not recoverable, while
checked Exceptions are recoverable. Java forces you to wrap code that
throws exceptions in try / catch
blocks, and when you catch them, your
application should fail gracefully. Either alert the user, if required;
relaunch the operation, if possible; and in all cases, log the situation.
Part 6: Wrapping Up
Did you find this book useful? Let me know!
11. Conclusion
I hope that the contents of this book have been useful to you. The Android development toolkit has evolved tremendously in the past 2 years, and new tools keep appearing every day. Long gone are the days of slow emulators, clunky IDEs and difficult installations. Android is now more approachable than ever!
Where to go from now? There is no better teacher than experience, so I suggest that you start porting your iOS applications to the Android environment, taking care of following the design guidelines from Google and making sure that you craft a great user experience. iOS and Android have some big differences in philosophy, so you should never just translate the source code when porting your apps. Make sure to follow and embrace the "Android way" at every step, to make your users comfortable and happy.
I look forward to downloading and using your apps!
Bibliography
This is the list of books and articles used during the preparation of this course.
-
[Bloch] Joshua Bloch. Effective Java: A Programming Language Guide – Second Edition. Addison-Wesley Professional. 2008. ISBN 978-0321356680.
-
[Darwin] Ian F. Darwin. Android Cookbook – Problems and Solutions for Android Developers. O’Reilly Media. 2012. ISBN 978-1-4493-8841-6.
-
[Kousen] Ken Kousen. Gradle Recipes for Android. O’Reilly Media. 2016. ISBN 978-1-4919-4702-9.
-
[Leiva] Antonio Leiva. Kotlin for Android Developers. Leanpub. 2016.
-
[Mcconnell] Steve McConnell. Code Complete, Second Edition. Microsoft Press. 2004. ISBN 0-7356-1967-0.
-
[Mednieks] Zigurd Mednieks, Laird Dornin, G. Blake Meike, and Masumi Nakamura. Programming Android 2nd Edition. O’Reilly Media. 2012. ISBN 978-1-449-31664-8.
-
[Phillips] Bill Phillips, Chris Stewart, Brian Hardy, Kristin Marsicano. Android Programming: The Big Nerd Ranch Guide, 2nd Edition. Big Nerd Ranch Guides. 2015. ISBN 978-0134171456.
-
[Sommerville] Ian Sommerville. Software Engineering, Eighth Edition. Addison Wesley. 2007. ISBN 978-0-321-31379-9.
-
[Barnes] Stephen Barnes. Android 101 for iOS Developers. Published in objc.io, April 2014.
-
[Cejas] Fernando Cejas. Architecting Android…The clean way?. Published in fernandocejas.com, September 2014.
-
[Farina] Nick Farina. An iOS Developer Takes on Android. Published in nfarina.com, August 2011.
-
[Hanning] Thomas Hanning. Android Development For iOS Developers – An Overview. Published in thomashanning.com, March 24th, 2016.
-
[Hoare] Sir Charles Anthony Richard Hoare. Null References: The Billion Dollar Mistake. Published in infoq.com, August 25th, 2009.
-
[iA] Information Architects. Starting Out on Android. Published in ia.net, February 25th, 2015.
-
[Kriplaney] Vikdram Kriplaney & Sebastián Vieira. Faceoff: Android vs. iOS. Published in realm.io, June 27th, 2016.
-
[Leiva] Antonio Leiva. RecyclerView in Android: The Basics. Published in antonioleiva.com, June 29th, 2014.
-
[Mottier] Cyril Mottier. Deep Dive Into Android State Restoration. Published in cyrilmottier.com, September 25th, 2014.
-
[Nakhimovich] Mike Nakhimovich. Improving Startup Time in the NYTimes Android App. Published in open.blogs.nytimes.com, February 11th, 2016.
-
[Poehls] Marcus Poehls. Retrofit Tutorial Series. Published in futurestud.io, December 1st, 2014.
-
[Puthraya] Thejaswi Puthraya. Developing android applications from command line. Published in agiliq.com, March 20th, 2012.
-
[Rodriguez] Fernando Rodriguez Romero. Learning Android Development: an iOS Developer’s Perspective. Published in bignerdranch.com, February 20th 2014.
-
[Savvy] Savvy. How to Start Android Development with an iOS Background. Published in savvyapps.com, March 17th 2016.
-
[Shekhar] Amit Shekhar. Android Development Best Practices. Published in Medium.com, September 30th, 2016.
-
[Smith] Craig Smith. 108 Amazing Android Statistics (November 2016). Published in expandedramblings.com, November 14th, 2016.
-
[soulseekah] soulseekah. Command Line Android Development: Debugging. Published in codeseekah.com, February 16th, 2012.
-
[Vogel] Lars Vogel. Android user interface testing with Espresso - Tutorial. Published in vogella.com, June 18th, 2016.
Appendix A: Android Studio Shortcuts
This appendix lists the most common Android Studio shortcuts. For a complete list, please refer to the Android Studio Keyboard Shortcuts web page.
Shortcut | Purpose |
---|---|
Cmd+L |
Go to line |
Option+Return |
Autocomplete imports |
Ctrl+Cmd+F12 |
Maximize/minimize editor |
Ctrl+G |
Start multiple select/replace session |
Cmd+O |
Find class |
Cmd+Shift+A |
Find action |
Shift+Shift |
Quick search everywhere |
Cmd+Shift+O |
Quick open file |
Option+↓ |
Jump to Lint issue in code |
Cmd+E |
Recently opened files pop-up |
Cmd+N |
Generate code (getters, setters, constructors, etc.) |
Ctrl+T |
Refactor selected element |
Shift+F6 |
Rename refactoring |
Cmd+O |
Override methods |
Cmd+D |
Duplicate current line |
Ctrl+Space |
Basic code completion |
Cmd+U |
Go to super method/class |
Cmd+/ |
Comment/uncomment current line(s) |
Cmd+Shift+/ |
Comment/uncomment current line(s) with C style |
Cmd+Option+L |
Reformat code |
Shift+Cmd+Option+L |
Reformat code dialog |
Control+Option+O |
Optimize imports |
Ctrl+R |
Build and run |
Ctrl+D |
Debug |
F8 |
Step over |
F7 |
Step into |
Cmd+F8 |
Toggle breakpoint |
Shift+Cmd+F8 |
Open Breakpoints window |
Cmd+Option+M |
Extract method |
Cmd+Shift+T |
Edit or create test for current class |
Ctrl+Shift+R |
Run current test |
Appendix B: Third Party Android Developer Tools
This appendix contains links to various third-party developer tools (some commercial, some open source and free) available to Android developers besides the standard Android Studio development environment described in this book.
Name | Programming Language | Link |
---|---|---|
Corona |
Lua |
|
Delphi |
Object Pascal |
|
Kivy |
Python |
|
Kotlin |
Kotlin |
|
Lazarus |
Free Pascal |
|
Processing |
Processing |
|
Qt |
C++ |
|
RubyMotion |
Ruby |
|
Scala on Android |
Scala |
|
Silver |
Swift |
|
Visual Studio |
C++ |
|
Xamarin |
C# |
Name | Kind | Link |
---|---|---|
ACRA |
Crash reporter |
|
Android Dev Metrics |
Performance library |
|
Android File Transfer |
File management |
|
Appium |
UI testing |
|
Briefs |
UI prototyping |
|
Butter Knife |
UI library |
|
Dagger |
Dependency injection |
|
Droid @ Screen |
Screen sharing |
|
FindBugs Static Analyzer |
Static analyzer |
|
Genymotion |
Android Emulator |
|
Glide |
Network library |
|
Gson |
Serialization library |
|
JUnit |
Unit testing |
|
LogCat |
Log viewer |
|
NimbleDroid |
Performance analysis service |
|
OkHttp |
Network library |
|
OrmLite |
Storage library |
|
Origami |
UI prototyping |
|
PaintCode |
UI prototyping |
|
PID cat |
Log viewer |
|
Realm |
Storage library |
|
Reflector 2 |
Screen sharing |
|
Retrofit |
Networking library |
|
Retrolambda |
Library |
|
Robolectric |
Unit testing runner |
|
Robotium |
UI testing |
|
Socket.io |
Network library |
|
SQLCipher for Android |
Storage library |
|
Stetho |
Debugging tool |
|
Vysor |
Screen sharing |
Appendix C: TL;DR
This appendix contains all the TL;DR tables at the beginning of each chapter, ready to print and keep within reach.
Android | iOS | |
---|---|---|
IDE |
Android Studio |
Xcode |
Profiling |
Android Device Monitor |
Instruments |
Preview |
Android Emulator |
iOS Simulator |
Blocks in previous versions |
Retrolambda |
PLBlocks |
Programming Language |
Java |
Swift or Objective-C |
Command Line |
|
|
Going beyond |
Rooting |
Jailbreaking |
Application metadata |
AndroidManifest.xml |
Info.plist |
Dependency Manager |
Gradle |
CocoaPods – Carthage |
Distribution |
APK |
IPA |
Debugger |
ADB + DDMS |
LLDB |
Logger |
LogCat |
NSLog() or print() |
View Debugging |
Hierarchy viewer |
Xcode view debugging |
Static Analysis |
Android Lint |
Clang Static Analyzer |
Classic programming language |
Java |
Objective-C |
Hype programming language |
Kotlin – Groovy – Scala – Clojure |
Swift |
Android | iOS | |
---|---|---|
Debugger |
JDB |
LLDB |
Log output |
logcat |
Xcode console |
Remote debugging |
yes |
no |
Log viewers |
PID Cat & LogCat |
libimobiledevice & deviceconsole |
Network logger |
NSLogger |
NSLogger |
Android | iOS | |
---|---|---|
UI design |
Layout files |
NIB/XIB/Storyboard |
Controllers |
Activity |
UIViewController |
Callbacks |
Anonymous Classes |
|
Views |
|
|
Connecting views |
|
|
Text fields |
|
|
Buttons |
|
|
Text labels |
|
|
Translatable strings |
strings.xml |
Localizable.strings |
Navigation between controllers |
Intent |
Storyboard Segue |
UI decomposition |
Fragment |
Children UIViewController |
Serialization |
|
|
Dialog boxes |
|
|
Android | iOS | |
---|---|---|
Framework |
|
UIKit |
Views |
|
|
Coordinate system |
Origin at top left |
Origin at top left |
Location on screen |
|
|
Images |
|
|
Colors |
|
|
Bezier curves |
|
|
Drawing method |
|
|
Drawing context |
|
|
Mark as "dirty" |
|
|
Gestures |
|
|
Pinch gesture |
|
|
Affine Transformations |
|
|
Simple animations |
|
|
Complex animations |
|
|
Application-level memory warnings |
|
|
Activity-level memory warnings |
|
|
Android | iOS | |
---|---|---|
Native networking library |
|
|
Background mechanism |
|
|
JSON Parser |
|
|
JSON (de)serialization |
Gson |
|
XML SAX |
|
|
XML DOM |
|
KissXML |
Array |
|
|
Table view |
|
|
Table view data |
|
|
Table view cell |
|
|
REST Client |
Retrofit |
RESTKit |
Popular networking library |
OkHttp |
AFNetworking |
Android | iOS | |
---|---|---|
Local documents |
|
|
External storage |
|
n/a |
Bundled resource |
|
|
Downloading files |
|
|
Notifications |
|
|
Periodic tasks |
|
|
Preferences |
|
|
Sqlite wrapper |
SQLiteOpenHelper |
FMDB |
ORM |
OrmLite |
Core Data |
Realm |
Realm |
Realm |
Android | iOS | |
---|---|---|
Framework |
|
Core Motion & Core Location |
Main class |
|
|
Callback methods |
|
Blocks |
Sensor data |
|
|
Location |
|
|
Android | iOS | |
---|---|---|
Display image |
|
|
Display video |
|
|
Viewer application |
Gallery |
Photos |
Image picker |
|
|
Audio recorder |
|
|
Audio player |
|
|
Text to speech engine |
|
|
Android | iOS | |
---|---|---|
Notification center |
|
|
Flexible designs |
Interface-oriented programming |
Protocol-oriented programming |
Dependency injection |
Dagger |
Typhoon – Swinject – Cleanse … |
ReactiveX |
RxJava & RxAndroid |
RxSwift |
Observing data |
|
|
Android | iOS | |
---|---|---|
Unit testing framework |
JUnit 4 |
XCTest |
Mock objects framework |
Mockito |
OCMock – OCMockito |
Most common issue |
|
(ObjC) Messages to nil & (Swift) optionals |
Appendix D: Supported Media Formats
This table summarizes the most important media formats supported by Android.[7]
Extension | Container Format | Data Format/Codec |
---|---|---|
.jpg |
JPEG |
JPEG |
.gif |
GIF |
GIF |
.png |
PNG |
PNG |
.bmp |
BMP |
BMP |
.webp |
WebP |
WebP |
Extension | Container Format | Data Format/Codec |
---|---|---|
.3gp – .mp4 – .m4a – .aac |
3GPP |
AAC – AMR |
.flac |
FLAC |
FLAC |
.mp3 |
MP3 |
MP3 |
.mid – .xmf – .rtx – .ota |
MIDI Type 0 – RTTTL/RTX |
MIDI |
.ogg |
Ogg |
Vorbis |
.wav |
PCM – WAVE |
WAVE |
.mkv |
Matroska |
Opus |
Extension | Container Format | Data Format/Codec |
---|---|---|
.3gp – .mp4 |
3GPP – MPEG-4 |
H.263 – H.265 |
.mp4 |
MPEG-4 |
H.265 |
.webm – .mkv |
WebM – Matroska |
VP8 – VP9 |