Android2?


Do not do any calculations inside the onDraw method of a view, you should instead finish drawing before calling
invalidate(). By using this technique you can avoid frame dropping in your view.
Rotations
The basic operations of a view are translate, rotate, etc... Almost every developer has faced this problem when they
GoalKicker.com – Android™ Notes for Professionals 217
use bitmap or gradients in their custom view. If the view is going to show a rotated view and the bitmap has to be
rotated in that custom view, many of us will think that it will be expensive. Many think that rotating a bitmap is very
expensive because in order to do that, you need to translate the bitmap's pixel matrix. But the truth is that it is not
that tough! Instead of rotating the bitmap, just rotate the canvas itself!
// Save the canvas state
int save = canvas.save();
// Rotate the canvas by providing the center point as pivot and angle
canvas.rotate(pivotX, pivotY, angle);
// Draw whatever you want
// Basically whatever you draw here will be drawn as per the angle you rotated the canvas
canvas.drawBitmap(...);
// Now restore your your canvas to its original state
canvas.restore(save);
// Unless canvas is restored to its original state, further draw will also be rotated.
Section 28.4: Creating a compound view
A compound view is a custom ViewGroup that's treated as a single view by the surrounding program code. Such a
ViewGroup can be really useful in DDD-like design, because it can correspond to an aggregate, in this example, a
Contact. It can be reused everywhere that contact is displayed.
This means that the surrounding controller code, an Activity, Fragment or Adapter, can simply pass the data object
to the view without picking it apart into a number of different UI widgets.
This facilitates code reuse and makes for a better design according to SOLID priciples.
The layout XML
This is usually where you start. You have an existing bit of XML that you find yourself reusing, perhaps as an
<include/>. Extract it into a separate XML file and wrap the root tag in a <merge> element:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/photo"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentRight="true" />
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/photo" />
<TextView
android:id="@+id/phone_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/name"
android:layout_toLeftOf="@id/photo" />
</merge>
This XML file keeps working in the Layout Editor in Android Studio perfectly fine. You can treat it like any other
GoalKicker.com – Android™ Notes for Professionals 218
layout.
The compound ViewGroup
Once you have the XML file, create the custom view group.
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.ImageView;
import android.widget.TextView;
import myapp.R;
/**
* A compound view to show contacts.
*
* This class can be put into an XML layout or instantiated programmatically, it
* will work correctly either way.
*/
public class ContactView extends RelativeLayout {
// This class extends RelativeLayout because that comes with an automatic
// (MATCH_PARENT, MATCH_PARENT) layout for its child item. You can extend
// the raw android.view.ViewGroup class if you want more control. See the
// note in the layout XML why you wouldn't want to extend a complex view
// such as RelativeLayout.
// 1. Implement superclass constructors.
public ContactView(Context context) {
super(context);
init(context, null);
}
// two extra constructors left out to keep the example shorter
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ContactView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
// 2. Initialize the view by inflating an XML using `this` as parent
private TextView mName;
private TextView mPhoneNumber;
private ImageView mPhoto;
private void init(Context context, AttributeSet attrs) {
LayoutInflater.from(context).inflate(R.layout.contact_view, this, true);
mName = (TextView) findViewById(R.id.name);
mPhoneNumber = (TextView) findViewById(R.id.phone_number);
mPhoto = (ImageView) findViewById(R.id.photo);
}
// 3. Define a setter that's expressed in your domain model. This is what the example is
// all about. All controller code can just invoke this setter instead of fiddling with
// lots of strings, visibility options, colors, animations, etc. If you don't use a
GoalKicker.com – Android™ Notes for Professionals 219
// custom view, this code will usually end up in a static helper method (bad) or copies
// of this code will be copy-pasted all over the place (worse).
public void setContact(Contact contact) {
mName.setText(contact.getName());
mPhoneNumber.setText(contact.getPhoneNumber());
if (contact.hasPhoto()) {
mPhoto.setVisibility(View.VISIBLE);
mPhoto.setImageBitmap(contact.getPhoto());
} else {
mPhoto.setVisibility(View.GONE);
}
}
}
The init(Context, AttributeSet) method is where you would read any custom XML attributes as explained in
Adding Attributes to Views.
With these pieces in place, you can use it in your app.
Usage in XML
Here's an example fragment_contact_info.xml that illustrates how you'd put a single ContactView on top of a list
of messages:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- The compound view becomes like any other view XML element -->
<myapp.ContactView
android:id="@+id/contact"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/message_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
Usage in Code
Here's an example RecyclerView.Adapter that shows a list of contacts. This example illustrates just how much
cleaner the controller code gets when it's completely free of View manipulation.
package myapp;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
public class ContactsAdapter extends RecyclerView.Adapter<ContactsViewHolder> {
private final Context context;
public ContactsAdapter(final Context context) {
this.context = context;
GoalKicker.com – Android™ Notes for Professionals 220
}
@Override
public ContactsViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ContactView v = new ContactView(context); // <--- this
return new ContactsViewHolder(v);
}
@Override
public void onBindViewHolder(ContactsViewHolder holder, int position) {
Contact contact = this.getItem(position);
holder.setContact(contact); // <--- this
}
static class ContactsViewHolder extends RecyclerView.ViewHolder {
public ContactsViewHolder(ContactView itemView) {
super(itemView);
}
public void setContact(Contact contact) {
((ContactView) itemView).setContact(contact); // <--- this
}
}
}
Section 28.5: Compound view for SVG/VectorDrawable as
drawableRight
Main motive to develop this compound view is, below 5.0 devices does not support svg in drawable inside
TextView/EditText. One more pros is, we can set height and width of drawableRight inside EditText. I have
separated it from my project and created in separate module. Module Name : custom_edit_drawable (short
name for prefix- c_d_e)
"c_d_e_" prefix to use so that app module resources should not override them by mistake. Example : "abc" prefix is
used by google in support library.
build.gradle
dependencies {
compile 'com.android.support:appcompat-v7:25.3.1'
}
use AppCompat >= 23
Layout file : c_e_d_compound_view.xml
<?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="wrap_content">
<EditText
android:id="@+id/edt_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1"
GoalKicker.com – Android™ Notes for Professionals 221
android:paddingEnd="40dp"
android:paddingLeft="5dp"
android:paddingRight="40dp"
android:paddingStart="5dp" />
<!--make sure you are not using ImageView instead of this-->
<android.support.v7.widget.AppCompatImageView
android:id="@+id/drawbleRight_search"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="right|center_vertical"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp" />
</FrameLayout>
Custom Attributes : attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="EditTextWithDrawable">
<attr name="c_e_d_drawableRightSVG" format="reference" />
<attr name="c_e_d_hint" format="string" />
<attr name="c_e_d_textSize" format="dimension" />
<attr name="c_e_d_textColor" format="color" />
</declare-styleable>
</resources>
Code : EditTextWithDrawable.java
public class EditTextWithDrawable extends FrameLayout {
public AppCompatImageView mDrawableRight;
public EditText mEditText;
public EditTextWithDrawable(Context context) {
super(context);
init(null);
}
public EditTextWithDrawable(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public EditTextWithDrawable(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public EditTextWithDrawable(Context context, AttributeSet attrs, int defStyleAttr, int
defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(attrs);
}
private void init(AttributeSet attrs) {
if (attrs != null && !isInEditMode()) {
LayoutInflater inflater = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.c_e_d_compound_view, this, true);
mDrawableRight = (AppCompatImageView) ((FrameLayout) getChildAt(0)).getChildAt(1);
GoalKicker.com – Android™ Notes for Professionals 222
mEditText = (EditText) ((FrameLayout) getChildAt(0)).getChildAt(0);
TypedArray attributeArray = getContext().obtainStyledAttributes(
attrs,
R.styleable.EditTextWithDrawable);
int drawableRes =
attributeArray.getResourceId(
R.styleable.EditTextWithDrawable_c_e_d_drawableRightSVG, -1);
if (drawableRes != -1) {
mDrawableRight.setImageResource(drawableRes);
}
mEditText.setHint(attributeArray.getString(
R.styleable.EditTextWithDrawable_c_e_d_hint));
mEditText.setTextColor(attributeArray.getColor(
R.styleable.EditTextWithDrawable_c_e_d_textColor, Color.BLACK));
int textSize =
attributeArray.getDimensionPixelSize(R.styleable.EditTextWithDrawable_c_e_d_textSize, 15);
mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
android.view.ViewGroup.LayoutParams layoutParams = mDrawableRight.getLayoutParams();
layoutParams.width = (textSize * 3) / 2;
layoutParams.height = (textSize * 3) / 2;
mDrawableRight.setLayoutParams(layoutParams);
attributeArray.recycle();
}
}
}
Example : How to use above view
Layout : activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.customeditdrawable.AppEditTextWithDrawable
android:id="@+id/edt_search_emp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:c_e_d_drawableRightSVG="@drawable/ic_svg_search"
app:c_e_d_hint="@string/hint_search_here"
app:c_e_d_textColor="@color/text_color_dark_on_light_bg"
app:c_e_d_textSize="@dimen/text_size_small" />
</LinearLayout>
Activity : MainActivity.java
public class MainActivity extends AppCompatActivity {
EditTextWithDrawable mEditTextWithDrawable;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mEditTextWithDrawable= (EditTextWithDrawable) findViewById(R.id.edt_search_emp);
}
}
GoalKicker.com – Android™ Notes for Professionals 223
Section 28.6: Responding to Touch Events
Many custom views need to accept user interaction in the form of touch events. You can get access to touch events
by overriding onTouchEvent. There are a number of actions you can filter out. The main ones are
ACTION_DOWN: This is triggered once when your finger first touches the view.
ACTION_MOVE: This is called every time your finger moves a little across the view. It gets called many times.
ACTION_UP: This is the last action to be called as you lift your finger off the screen.
You can add the following method to your view and then observe the log output when you touch and move your
finger around your view.
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.i("CustomView", "onTouchEvent: ACTION_DOWN: x = " + x + ", y = " + y);
break;
case MotionEvent.ACTION_MOVE:
Log.i("CustomView", "onTouchEvent: ACTION_MOVE: x = " + x + ", y = " + y);
break;
case MotionEvent.ACTION_UP:
Log.i("CustomView", "onTouchEvent: ACTION_UP: x = " + x + ", y = " + y);
break;
}
return true;
}
Further reading:
Android official documentation: Responding to Touch Events
GoalKicker.com – Android™ Notes for Professionals 224
Chapter 29: Getting Calculated View
Dimensions
Section 29.1: Calculating initial View dimensions in an Activity
package com.example;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
public class ExampleActivity extends Activity {
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_example);
final View viewToMeasure = findViewById(R.id.view_to_measure);
// viewToMeasure dimensions are not known at this point.
// viewToMeasure.getWidth() and viewToMeasure.getHeight() both return 0,
// regardless of on-screen size.
viewToMeasure.getViewTreeObserver().addOnPreDrawListener(new
ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
// viewToMeasure is now measured and laid out, and displayed dimensions are known.
logComputedViewDimensions(viewToMeasure.getWidth(), viewToMeasure.getHeight());
// Remove this listener, as we have now successfully calculated the desired
dimensions.
viewToMeasure.getViewTreeObserver().removeOnPreDrawListener(this);
// Always return true to continue drawing.
return true;
}
});
}
private void logComputedViewDimensions(final int width, final int height) {
Log.d("example", "viewToMeasure has width " + width);
Log.d("example", "viewToMeasure has height " + height);
}
}
GoalKicker.com – Android™ Notes for Professionals 225
Chapter 30: Adding a FuseView to an
Android Project
Export a Fuse.View from fusetools and use it inside an existing android project.
Our goal is to export the entire hikr sample app and use it inside an Activity.
Final work can be found @lucamtudor/hikr-fuse-view
Section 30.1: hikr app, just another android.view.View
Prerequisites
you should have fuse installed (https://www.fusetools.com/downloads)
you should have done the introduction tutorial
in terminal: fuse install android
in terminal: uno install Fuse.Views
Step 1
git clone https://github.com/fusetools/hikr
Step 2 : Add package reference to Fuse.Views
Find hikr.unoproj file inside the project root folder and add "Fuse.Views" to the "Packages" array.
{
"RootNamespace":"",
"Packages": [
"Fuse",
"FuseJS",
"Fuse.Views"
],
"Includes": [
"*",
"Modules/*.js:Bundle"
]
}
Step 3 : Make HikrApp component to hold the entire app
3.1 In the project root folder make a new file called HikrApp.ux and paste the contents of MainView.ux.
HikrApp.ux
<App Background="#022328">
<iOS.StatusBarConfig Style="Light" />
<Android.StatusBarConfig Color="#022328" />
<Router ux:Name="router" />
<ClientPanel>
<Navigator DefaultPath="splash">
GoalKicker.com – Android™ Notes for Professionals 226
<SplashPage ux:Template="splash" router="router" />
<HomePage ux:Template="home" router="router" />
<EditHikePage ux:Template="editHike" router="router" />
</Navigator>
</ClientPanel>
</App>
3.2 In HikrApp.ux
replace the <App> tags with <Page>
add ux:Class="HikrApp" to the opening <Page>
remove <ClientPanel>, we don't have to worry anymore about the status bar or the bottom nav buttons
HikrApp.ux
<Page ux:Class="HikrApp" Background="#022328">
<iOS.StatusBarConfig Style="Light" />
<Android.StatusBarConfig Color="#022328" />
<Router ux:Name="router" />
<Navigator DefaultPath="splash">
<SplashPage ux:Template="splash" router="router" />
<HomePage ux:Template="home" router="router" />
<EditHikePage ux:Template="editHike" router="router" />
</Navigator>
</Page>
3.3 Use the newly created HikrApp component inside MainView.ux
Replace the content of MainView.ux file with:
<App>
<HikrApp/>
</App>
Our app is back to its normal behavior, but we now have extracted it to a separate component called HikrApp
Step 4 Inside MainView.ux replace the <App> tags with <ExportedViews> and add ux:Template="HikrAppView" to
<HikrApp />
<ExportedViews>
<HikrApp ux:Template="HikrAppView" />
</ExportedViews>
Remember the template HikrAppView, because we'll need it to get a reference to our view from Java.
Note. From the fuse docs:
ExportedViews will behave as App when doing normal fuse preview and uno build
Not true. You will get this error when previewing from Fuse Studio:
GoalKicker.com – Android™ Notes for Professionals 227
Error: Couldn't find an App tag in any of the included UX files. Have you forgot to include the UX file that
contains the app tag?
Step 5 Wrap SplashPage.ux's <DockPanel> in a <GraphicsView>
<Page ux:Class="SplashPage">
<Router ux:Dependency="router" />
<JavaScript File="SplashPage.js" />
<GraphicsView>
<DockPanel ClipToBounds="true">
<Video Layer="Background" File="../Assets/nature.mp4" IsLooping="true" AutoPlay="true"
StretchMode="UniformToFill" Opacity="0.5">
<Blur Radius="4.75" />
</Video>
<hikr.Text Dock="Bottom" Margin="10" Opacity=".5" TextAlignment="Center"
FontSize="12">original video by Graham Uhelski</hikr.Text>
<Grid RowCount="2">
<StackPanel Alignment="VerticalCenter">
<hikr.Text Alignment="HorizontalCenter" FontSize="70">hikr</hikr.Text>
<hikr.Text Alignment="HorizontalCenter" Opacity=".5">get out there</hikr.Text>
</StackPanel>
<hikr.Button Text="Get Started" FontSize="18" Margin="50,0"
Alignment="VerticalCenter" Clicked="{goToHomePage}" />
</Grid>
</DockPanel>
</GraphicsView>
</Page>
Step 6 Export the fuse project as an aar library
in terminal, in root project folder: uno clean
in terminal, in root project folder: uno build -t=android -DLIBRARY
Step 7 Prepare your android project
copy the aar from .../rootHikeProject/build/Android/Debug/app/build/outputs/aar/app-debug.aar to
.../androidRootProject/app/libs
add flatDir { dirs 'libs' } to the root build.gradle file
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { ... }
...
allprojects {
repositories {
jcenter()
GoalKicker.com – Android™ Notes for Professionals 228
flatDir {
dirs 'libs'
}
}
}
...
add compile(name: 'app-debug', ext: 'aar') to dependencies in app/build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "com.shiftstudio.fuseviewtest"
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(name: 'app-debug', ext: 'aar')
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.3.1'
testCompile 'junit:junit:4.12'
}
add the following properties to the activity inside AndroidManifest.xml
android:launchMode="singleTask"
android:taskAffinity=""
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize"
Your AndroidManifest.xml will look like this:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.shiftstudio.fuseviewtest">
<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"
GoalKicker.com – Android™ Notes for Professionals 229
android:launchMode="singleTask"
android:taskAffinity=""
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Step 8: Show the Fuse.View HikrAppView in your Activity
note that your Activity needs to inherit FuseViewsActivity
public class MainActivity extends FuseViewsActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ViewHandle fuseHandle = ExportedViews.instantiate("HikrAppView");
final FrameLayout root = (FrameLayout) findViewById(R.id.fuse_root);
final View fuseApp = fuseHandle.getView();
root.addView(fuseApp);
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.shiftstudio.fuseviewtest.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="24sp"
android:textStyle="bold"
android:layout_height="wrap_content"
android:text="Hello World, from Kotlin" />
<FrameLayout
android:id="@+id/fuse_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
GoalKicker.com – Android™ Notes for Professionals 230
<TextView
android:layout_width="wrap_content"
android:text="THIS IS FROM NATIVE.\nBEHIND FUSE VIEW"
android:layout_gravity="center"
android:textStyle="bold"
android:textSize="30sp"
android:background="@color/colorAccent"
android:textAlignment="center"
android:layout_height="wrap_content" />
</FrameLayout>
</LinearLayout>
Note
When you press the back button, on android, the app crashes. You can follow the issue on the fuse forum.
A/libc: Fatal signal 11 (SIGSEGV), code 1, fault addr 0xdeadcab1 in tid 18026 (io.fuseviewtest)
[ 05-25 11:52:33.658 16567:16567 W/ ]
debuggerd: handling request: pid=18026 uid=10236 gid=10236 tid=18026
And the final result is something like this. You can also find a short clip on github.
GoalKicker.com – Android™ Notes for Professionals 231
GoalKicker.com – Android™ Notes for Professionals 232
Chapter 31: Supporting Screens With
Dierent Resolutions, Sizes
Section 31.1: Using configuration qualifiers
Android supports several configuration qualifiers that allow you to control how the system selects your alternative
resources based on the characteristics of the current device screen. A configuration qualifier is a string that you can
append to a resource directory in your Android project and specifies the configuration for which the resources
inside are designed.
To use a configuration qualifier:
1. Create a new directory in your project's res/ directory and name it using the format: <resources_name>-
<qualifier>. <resources_name> is the standard resource name (such as drawable or layout).
2. <qualifier> is a configuration qualifier, specifying the screen configuration for which these resources are to
be used (such as hdpi or xlarge).
For example, the following application resource directories provide different layout designs for different screen
sizes and different drawables. Use the mipmap/ folders for launcher icons.
res/layout/my_layout.xml // layout for normal screen size ("default")
res/layout-large/my_layout.xml // layout for large screen size
res/layout-xlarge/my_layout.xml // layout for extra-large screen size
res/layout-xlarge-land/my_layout.xml // layout for extra-large in landscape orientation
res/drawable-mdpi/graphic.png // bitmap for medium-density
res/drawable-hdpi/graphic.png // bitmap for high-density
res/drawable-xhdpi/graphic.png // bitmap for extra-high-density
res/drawable-xxhdpi/graphic.png // bitmap for extra-extra-high-density
res/mipmap-mdpi/my_icon.png // launcher icon for medium-density
res/mipmap-hdpi/my_icon.png // launcher icon for high-density
res/mipmap-xhdpi/my_icon.png // launcher icon for extra-high-density
res/mipmap-xxhdpi/my_icon.png // launcher icon for extra-extra-high-density
res/mipmap-xxxhdpi/my_icon.png // launcher icon for extra-extra-extra-high-density
Section 31.2: Converting dp and sp to pixels
When you need to set a pixel value for something like Paint.setTextSize but still want it be scaled based on the
device, you can convert dp and sp values.
DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
float pixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12f, metrics);
DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
float pixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 12f, metrics);
Alternatively, you can convert a dimension resource to pixels if you have a context to load the resource from.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="size_in_sp">12sp</dimen>
<dimen name="size_in_dp">12dp</dimen>
</resources>
GoalKicker.com – Android™ Notes for Professionals 233
// Get the exact dimension specified by the resource
float pixels = context.getResources().getDimension(R.dimen.size_in_sp);
float pixels = context.getResources().getDimension(R.dimen.size_in_dp);
// Get the dimension specified by the resource for use as a size.
// The value is rounded down to the nearest integer but is at least 1px.
int pixels = context.getResources().getDimensionPixelSize(R.dimen.size_in_sp);
int pixels = context.getResources().getDimensionPixelSize(R.dimen.size_in_dp);
// Get the dimension specified by the resource for use as an offset.
// The value is rounded down to the nearest integer and can be 0px.
int pixels = context.getResources().getDimensionPixelOffset(R.dimen.size_in_sp);
int pixels = context.getResources().getDimensionPixelOffset(R.dimen.size_in_dp);
Section 31.3: Text size and dierent android screen sizes
Sometimes, it's better to have only three options
style="@android:style/TextAppearance.Small"
style="@android:style/TextAppearance.Medium"
style="@android:style/TextAppearance.Large"
Use small and large to differentiate from normal screen size.
<TextView
android:id="@+id/TextViewTopBarTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@android:style/TextAppearance.Small"/>
For normal, you don't have to specify anything.
<TextView
android:id="@+id/TextViewTopBarTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
Using this, you can avoid testing and specifying dimensions for different screen sizes.
GoalKicker.com – Android™ Notes for Professionals 234
Chapter 32: ViewFlipper
A ViewFlipper is a ViewAnimator that switches between two or more views that have been added to it. Only one
child is shown at a time. If requested, the ViewFlipper can automatically flip between each child at a regular
interval.
Section 32.1: ViewFlipper with image sliding
XML file:
<ViewFlipper
android:id="@+id/viewflip"
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_weight="1"
/>
Java code:
public class BlankFragment extends Fragment{
ViewFlipper viewFlipper;
FragmentManager fragmentManager;
int gallery_grid_Images[] = {drawable.image1, drawable.image2, drawable.image3,
drawable.image1, drawable.image2, drawable.image3, drawable.image1,
drawable.image2, drawable.image3, drawable.image1
};
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
savedInstanceState){
View rootView = inflater.inflate(fragment_blank, container, false);
viewFlipper = (ViewFlipper)rootView.findViewById(R.id.viewflip);
for(int i=0; i<gallery_grid_Images.length; i++){
// This will create dynamic image views and add them to the ViewFlipper.
setFlipperImage(gallery_grid_Images[i]);
}
return rootView;
}
private void setFlipperImage(int res) {
Log.i("Set Filpper Called", res+"");
ImageView image = new ImageView(getContext());
image.setBackgroundResource(res);
viewFlipper.addView(image);
viewFlipper.setFlipInterval(1000);
viewFlipper.setAutoStart(true);
}
}
GoalKicker.com – Android™ Notes for Professionals 235
Chapter 33: Design Patterns
Design patterns are formalized best practices that the programmer can use to solve common problems when
designing an application or system.
Design patterns can speed up the development process by providing tested, proven development paradigms.
Reusing design patterns helps to prevent subtle issues that can cause major problems, and it also improves code
readability for coders and architects who are familiar with the patterns.
Section 33.1: Observer pattern
The observer pattern is a common pattern, which is widely used in many contexts. A real example can be taken
from YouTube: When you like a channel and want to get all news and watch new videos from this channel, you have
to subscribe to that channel. Then, whenever this channel publishes any news, you (and all other subscribers) will
receive a notification.
An observer will have two components. One is a broadcaster (channel) and the other is a receiver (you or any other
subscriber). The broadcaster will handle all receiver instances that subscribed to it. When the broadcaster fires a
new event, it will announce this to all receiver instances. When the receiver receives an event, it will have to react to
that event, for example, by turning on YouTube and playing the new video.
Implementing the observer pattern
1. The broadcaster has to provide methods that permit receivers to subscribe and unsubscribe to it. When the
broadcaster fires an event, subscribers need to be notified that an event has occurred:
class Channel{
private List<Subscriber> subscribers;
public void subscribe(Subscriber sub) {
// Add new subscriber.
}
public void unsubscribe(Subscriber sub) {
// Remove subscriber.
}
public void newEvent() {
// Notification event for all subscribers.
}
}
2. The receiver needs to implement a method that handles the event from the broadcaster:
interface Subscriber {
void doSubscribe(Channel channel);
void doUnsubscribe(Channel channel);
void handleEvent(); // Process the new event.
}
Section 33.2: Singleton Class Example
Java Singleton Pattern
To implement Singleton pattern, we have different approaches but all of them have following common concepts.
GoalKicker.com – Android™ Notes for Professionals 236
Private constructor to restrict instantiation of the class from other classes.
Private static variable of the same class that is the only instance of the class.
Public static method that returns the instance of the class, this is the global access
point for outer world to get the instance of the singleton class.
/**
* Singleton class.
*/
public final class Singleton {
/**
* Private constructor so nobody can instantiate the class.
*/
private Singleton() {}
/**
* Static to class instance of the class.
*/
private static final Singleton INSTANCE = new Singleton();
/**
* To be called by user to obtain instance of the class.
*
* @return instance of the singleton.
*/
public static Singleton getInstance() {
return INSTANCE;
}
}
GoalKicker.com – Android™ Notes for Professionals 237
Chapter 34: Activity
Parameter Details
Intent Can be used with startActivity to launch an Activity
Bundle A mapping from String keys to various Parcelable values.
Context Interface to global information about an application environment.
An Activity represents a single screen with a user interface(UI). An Android App may have more than one Activity,
for example, An email App can have one activity to list all the emails, another activity to show email contents, yet
another activity to compose new email. All the activities in an App work together to create perfect user experience.
Section 34.1: Activity launchMode
Launch mode defines the behaviour of new or existing activity in the task.
There are possible launch modes:
standard
singleTop
singleTask
singleInstance
It should be defined in android manifest in <activity/> element as android:launchMode attribute.
<activity
android:launchMode=["standard" | "singleTop" | "singleTask" | "singleInstance"] />
Standard:
Default value. If this mode set, new activity will always be created for each new intent. So it's possible to get many
activities of same type. New activity will be placed on the top of the task. There is some difference for different
android version: if activity is starting from another application, on androids <= 4.4 it will be placed on same task as
starter application, but on >= 5.0 new task will be created.
SingleTop:
This mode is almost the same as standard. Many instances of singleTop activity could be created. The difference is,
if an instance of activity already exists on the top of the current stack, onNewIntent() will be called instead of
creating new instance.
SingleTask:
Activity with this launch mode can have only one instance in the system. New task for activity will be created, if it
doesn't exist. Otherwise, task with activity will be moved to front and onNewIntent will be called.
SingleInstance:
This mode is similar to singleTask. The difference is task that holds an activity with singleInstance could have
only this activity and nothing more. When singleInstance activity create another activity, new task will be created
to place that activity.
GoalKicker.com – Android™ Notes for Professionals 238
Section 34.2: Exclude an activity from back-stack history
Let there be Activity B that can be opened, and can further start more Activities. But, user should not encounter it
when navigating back in task activities.
The simplest solution is to set the attribute noHistory to true for that <activity> tag in AndroidManifest.xml:
<activity
android:name=".B"
android:noHistory="true">
This same behavior is also possible from code if B calls finish() before starting the next activity:
finish();
startActivity(new Intent(context, C.class));
Typical usage of noHistory flag is with "Splash Screen" or Login Activities.
Section 34.3: Android Activity LifeCycle Explained
Assume an application with a MainActivity which can call the Next Activity using a button click.
public class MainActivity extends AppCompatActivity {
private final String LOG_TAG = MainActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
GoalKicker.com – Android™ Notes for Professionals 239
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(LOG_TAG, "calling onCreate from MainActivity");
}
@Override
protected void onStart() {
super.onStart();
Log.d(LOG_TAG, "calling onStart from MainActivity");
}
@Override
protected void onResume() {
super.onResume();
Log.d(LOG_TAG, "calling onResume from MainActivity");
}
@Override
protected void onPause() {
super.onPause();
Log.d(LOG_TAG, "calling onPause from MainActivity");
}
@Override
protected void onStop() {
super.onStop();
Log.d(LOG_TAG, "calling onStop from MainActivity");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(LOG_TAG, "calling onDestroy from MainActivity");
}
@Override
protected void onRestart() {
super.onRestart();
Log.d(LOG_TAG, "calling onRestart from MainActivity");
}
public void toNextActivity(){
Log.d(LOG_TAG, "calling Next Activity");
Intent intent = new Intent(this, NextActivity.class);
startActivity(intent);
} }
and
public class NextActivity extends AppCompatActivity {
private final String LOG_TAG = NextActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_next);
Log.d(LOG_TAG, "calling onCreate from Next Activity");
}
@Override
protected void onStart() {
super.onStart();
Log.d(LOG_TAG, "calling onStart from Next Activity");
}
@Override
protected void onResume() {
GoalKicker.com – Android™ Notes for Professionals 240
super.onResume();
Log.d(LOG_TAG, "calling onResume from Next Activity");
}
@Override
protected void onPause() {
super.onPause();
Log.d(LOG_TAG, "calling onPause from Next Activity");
}
@Override
protected void onStop() {
super.onStop();
Log.d(LOG_TAG, "calling onStop from Next Activity");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(LOG_TAG, "calling onDestroy from Next Activity");
}
@Override
protected void onRestart() {
super.onRestart();
Log.d(LOG_TAG, "calling onRestart from Next Activity");
} }
When app is first created
D/MainActivity: calling onCreate from MainActivity
D/MainActivity: calling onStart from MainActivity
D/MainActivity: calling onResume from MainActivity
are called
When screen sleeps
08:11:03.142 D/MainActivity: calling onPause from MainActivity
08:11:03.192 D/MainActivity: calling onStop from MainActivity
are called. And again when it wakes up
08:11:55.922 D/MainActivity: calling onRestart from MainActivity
08:11:55.962 D/MainActivity: calling onStart from MainActivity
08:11:55.962 D/MainActivity: calling onResume from MainActivity
are called
Case1: When Next Activity is called from Main Activity
D/MainActivity: calling Next Activity
D/MainActivity: calling onPause from MainActivity
D/NextActivity: calling onCreate from Next Activity
D/NextActivity: calling onStart from Next Activity
D/NextActivity: calling onResume from Next Activity
D/MainActivity: calling onStop from MainActivity
When Returning back to the Main Activity from Next Activity using back button
D/NextActivity: calling onPause from Next Activity
D/MainActivity: calling onRestart from MainActivity
D/MainActivity: calling onStart from MainActivity
D/MainActivity: calling onResume from MainActivity
GoalKicker.com – Android™ Notes for Professionals 241
D/NextActivity: calling onStop from Next Activity
D/NextActivity: calling onDestroy from Next Activity
Case2: When Activity is partially obscured (When overview button is pressed) or When app goes to background and
another app completely obscures it
D/MainActivity: calling onPause from MainActivity
D/MainActivity: calling onStop from MainActivity
and when the app is back in the foreground ready to accept User inputs,
D/MainActivity: calling onRestart from MainActivity
D/MainActivity: calling onStart from MainActivity
D/MainActivity: calling onResume from MainActivity
are called
Case3: When an activity is called to fulfill implicit intent and user has make a selection. For eg., when share button is
pressed and user has to select an app from the list of applications shown
D/MainActivity: calling onPause from MainActivity
The activity is visible but not active now. When the selection is done and app is active
D/MainActivity: calling onResume from MainActivity
is called
Case4:
When the app is killed in the background(to free resources for another foreground app), onPause(for prehoneycomb
device) or onStop(for since honeycomb device) will be the last to be called before the app is terminated.
onCreate and onDestroy will be called utmost once each time the application is run. But the onPause, onStop,
onRestart, onStart, onResume maybe called many times during the lifecycle.
Section 34.4: End Application with exclude from Recents
First define an ExitActivity in the AndroidManifest.xml
<activity
android:name="com.your_example_app.activities.ExitActivity"
android:autoRemoveFromRecents="true"
android:theme="@android:style/Theme.NoDisplay" />
Afterwards the ExitActivity-class
/**
* Activity to exit Application without staying in the stack of last opened applications
*/
public class ExitActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Utils.hasLollipop()) {
finishAndRemoveTask();
} else if (Utils.hasJellyBean()) {
finishAffinity();
} else {
finish();
}
}
GoalKicker.com – Android™ Notes for Professionals 242
/**
* Exit Application and Exclude from Recents
*
* @param context Context to use
*/
public static void exitApplication(ApplicationContext context) {
Intent intent = new Intent(context, ExitActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
context.startActivity(intent);
}
}
Section 34.5: Presenting UI with setContentView
Activity class takes care of creating a window for you in which you can place your UI with setContentView.
There are three setContentView methods:
setContentView(int layoutResID) - Set the activity content from a layout resource.
setContentView(View view) - Set the activity content to an explicit view.
setContentView(View view, ViewGroup.LayoutParams params) - Set the activity content to an explicit view
with provided params.
When setContentView is called, this view is placed directly into the activity's view hierarchy. It can itself be a
complex view hierarchy.
Examples
Set content from resource file:
Add resource file (main.xml in this example) with view hierarchy:
<?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" >
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello" />
</FrameLayout>
Set it as content in activity:
public final class MainActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// The resource will be inflated,
// adding all top-level views to the activity.
setContentView(R.layout.main);
}
}
Set content to an explicit view:
GoalKicker.com – Android™ Notes for Professionals 243
public final class MainActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Creating view with container
final FrameLayout root = new FrameLayout(this);
final TextView text = new TextView(this);
text.setText("Hello");
root.addView(text);
// Set container as content view
setContentView(root);
}
}
Section 34.6: Up Navigation for Activities
Up navigation is done in android by adding android:parentActivityName="" in Manifest.xml to the activity tag.
Basically with this tag you tell the system about the parent activity of a activity.
How is it done?
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".SkillSchoolApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".ui.activities.SplashActivity"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ui.activities.MainActivity" />
<activity android:name=".ui.activities.HomeActivity"
android:parentActivityName=".ui.activities.MainActivity/> // HERE I JUST TOLD THE SYSTEM THAT
MainActivity is the parent of HomeActivity
</application>
Now when I will click on the arrow inside the toolbar of HomeActivity it will take me back to the parent activity.
Java Code
Here I will write the appropriate Java code required for this functionality.
public class HomeActivity extends AppCompatActivity {
@BindView(R.id.toolbar)
Toolbar toolbar;
GoalKicker.com – Android™ Notes for Professionals 244
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
ButterKnife.bind(this);
//Since i am using custom tool bar i am setting refernce of that toolbar to Actionbar. If you
are not using custom then you can simple leave this and move to next line
setSupportActionBar(toolbar);
getSupportActionBar.setDisplayHomeAsUpEnabled(true); // this will show the back arrow in
the tool bar.
}
}
If you run this code you will see when you press back button it will take you back to MainActivity. For futher
understanding of Up Navigation i would recommend reading docs
You can more customize this behaviour upon your needs by overriding
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
// Respond to the action bar's Up/Home button
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this); // Here you will write your logic for handling up
navigation
return true;
}
return super.onOptionsItemSelected(item);
}
Simple Hack
This is simple hack which is mostly used to navigate to parent activity if parent is in backstack. By calling
onBackPressed() if id is equal to android.R.id.home
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
switch (id) {
case android.R.id.home:
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
Section 34.7: Clear your current Activity stack and launch a
new Activity
If you want to clear your current Activity stack and launch a new Activity (for example, logging out of the app and
launching a log in Activity), there appears to be two approaches.
1. Target (API >= 16)
Calling finishAffinity() from an Activity
2. Target (11 <= API < 16)
GoalKicker.com – Android™ Notes for Professionals 245
Intent intent = new Intent(this, LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
|Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
GoalKicker.com – Android™ Notes for Professionals 246
Chapter 35: Activity Recognition
Activity recognition is the detection of a user's physical activity in order to perform certain actions on the device,
such as taking points when a drive is detected, turn wifi off when a phone is still, or putting the ring volume to max
when the user is walking.
Section 35.1: Google Play ActivityRecognitionAPI
This is a just a simple example of how to use GooglePlay Service's ActivityRecognitionApi. Although this is a great
library, it does not work on devices that do not have Google Play Services installed.
Docs for ActivityRecognition API
Manifest
<!-- This is needed to use Activity Recognition! -->
<uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
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>
<receiver android:name=".ActivityReceiver" />
</application>
MainActivity.java
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
private GoogleApiClient apiClient;
private LocalBroadcastManager localBroadcastManager;
private BroadcastReceiver localActivityReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
apiClient = new GoogleApiClient.Builder(this)
.addApi(ActivityRecognition.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
//This just gets the activity intent from the ActivityReceiver class
localBroadcastManager = LocalBroadcastManager.getInstance(this);
GoalKicker.com – Android™ Notes for Professionals 247
localActivityReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
ActivityRecognitionResult recognitionResult =
ActivityRecognitionResult.extractResult(intent);
TextView textView = (TextView) findViewById(R.id.activityText);
//This is just to get the activity name. Use at your own risk.
textView.setText(DetectedActivity.zzkf(recognitionResult.getMostProbableActivity().getType()));
}
};
}
@Override
protected void onResume() {
super.onResume();
//Register local broadcast receiver
localBroadcastManager.registerReceiver(localActivityReceiver, new
IntentFilter("activity"));
//Connect google api client
apiClient.connect();
}
@Override
protected void onPause() {
super.onPause();
//Unregister for activity recognition
ActivityRecognition.ActivityRecognitionApi.removeActivityUpdates(apiClient,
PendingIntent.getBroadcast(this, 0, new Intent(this, ActivityReceiver.class),
PendingIntent.FLAG_UPDATE_CURRENT));
//Disconnects api client
apiClient.disconnect();
//Unregister local receiver
localBroadcastManager.unregisterReceiver(localActivityReceiver);
}
@Override
public void onConnected(@Nullable Bundle bundle) {
//Only register for activity recognition if google api client has connected
ActivityRecognition.ActivityRecognitionApi.requestActivityUpdates(apiClient, 0,
PendingIntent.getBroadcast(this, 0, new Intent(this, ActivityReceiver.class),
PendingIntent.FLAG_UPDATE_CURRENT));
}
@Override
public void onConnectionSuspended(int i) {
}
@Override
public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
}
}
ActivityReceiver
GoalKicker.com – Android™ Notes for Professionals 248
public class ActivityReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
LocalBroadcastManager.getInstance(context).sendBroadcast(intent.setAction("activity"));
}
}
Section 35.2: PathSense Activity Recognition
PathSense activity recognition is another good library for devices which don't have Google Play Services, as they
have built their own activity recognition model, but requires developers register at http://developer.pathsense.com
to get an API key and Client ID.
Manifest
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
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>
<receiver android:name=".ActivityReceiver" />
<!-- You need to acquire these from their website (http://developer.pathsense.com) -->
<meta-data
android:name="com.pathsense.android.sdk.CLIENT_ID"
android:value="YOUR_CLIENT_ID" />
<meta-data
android:name="com.pathsense.android.sdk.API_KEY"
android:value="YOUR_API_KEY" />
</application>
MainActivity.java
public class MainActivity extends AppCompatActivity {
private PathsenseLocationProviderApi pathsenseLocationProviderApi;
private LocalBroadcastManager localBroadcastManager;
private BroadcastReceiver localActivityReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
pathsenseLocationProviderApi = PathsenseLocationProviderApi.getInstance(this);
//This just gets the activity intent from the ActivityReceiver class
localBroadcastManager = LocalBroadcastManager.getInstance(this);
GoalKicker.com – Android™ Notes for Professionals 249
localActivityReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
//The detectedActivities object is passed as a serializable
PathsenseDetectedActivities detectedActivities = (PathsenseDetectedActivities)
intent.getSerializableExtra("ps");
TextView textView = (TextView) findViewById(R.id.activityText);
textView.setText(detectedActivities.getMostProbableActivity().getDetectedActivity().name());
}
};
}
@Override
protected void onResume() {
super.onResume();
//Register local broadcast receiver
localBroadcastManager.registerReceiver(localActivityReceiver, new
IntentFilter("activity"));
//This gives an update every time it receives one, even if it was the same as the last update
pathsenseLocationProviderApi.requestActivityUpdates(ActivityReceiver.class);
// This gives updates only when it changes (ON_FOOT -> IN_VEHICLE for example)
// pathsenseLocationProviderApi.requestActivityChanges(ActivityReceiver.class);
}
@Override
protected void onPause() {
super.onPause();
pathsenseLocationProviderApi.removeActivityUpdates();
// pathsenseLocationProviderApi.removeActivityChanges();
//Unregister local receiver
localBroadcastManager.unregisterReceiver(localActivityReceiver);
}
}
ActivityReceiver.java
// You don't have to use their broadcastreceiver, but it's best to do so, and just pass the result
// as needed to another class.
public class ActivityReceiver extends PathsenseActivityRecognitionReceiver {
@Override
protected void onDetectedActivities(Context context, PathsenseDetectedActivities
pathsenseDetectedActivities) {
Intent intent = new Intent("activity").putExtra("ps", pathsenseDetectedActivities);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
}
GoalKicker.com – Android™ Notes for Professionals 250
Chapter 36: Split Screen / Multi-Screen
Activities
Section 36.1: Split Screen introduced in Android Nougat
implemented
Set this attribute in your manifest's or element to enable or disable multi-window display:
android:resizeableActivity=["true" | "false"]
If this attribute is set to true, the activity can be launched in split-screen and freeform modes. If the attribute is set
to false, the activity does not support multi-window mode. If this value is false, and the user attempts to launch the
activity in multi-window mode, the activity takes over the full screen.
If your app targets API level 24, but you do not specify a value for this attribute, the attribute's value defaults to
true.
The following code shows how to specify an activity's default size and location, and its minimum size, when the
activity is displayed in freeform mode:
<--These are default values suggested by google.-->
<activity android:name=".MyActivity">
<layout android:defaultHeight="500dp"
android:defaultWidth="600dp"
android:gravity="top|end"
android:minHeight="450dp"
android:minWidth="300dp" />
</activity>
Disabled features in multi-window mode
Certain features are disabled or ignored when a device is in multi-window mode, because they don’t make sense
for an activity which may be sharing the device screen with other activities or apps. Such features include:
1. Some System UI customization options are disabled; for example, apps cannot hide the status bar if they are
not running in full-screen mode.
2. The system ignores changes to the android:screenOrientation attribute.
If your app targets API level 23 or lower
If your app targets API level 23 or lower and the user attempts to use the app in multi-window mode, the system
forcibly resizes the app unless the app declares a fixed orientation.
If your app does not declare a fixed orientation, you should launch your app on a device running Android 7.0 or
higher and attempt to put the app in split-screen mode. Verify that the user experience is acceptable when the app
is forcibly resized.
If the app declares a fixed orientation, you should attempt to put the app in multi-window mode. Verify that when
you do so, the app remains in full-screen mode.
GoalKicker.com – Android™ Notes for Professionals 251
Chapter 37: Material Design
Material Design is a comprehensive guide for visual, motion, and interaction design across platforms and devices.
Section 37.1: Adding a Toolbar
A Toolbar is a generalization of ActionBar for use within application layouts. While an ActionBar is traditionally
part of an Activity's opaque window decor controlled by the framework, a Toolbar may be placed at any
arbitrary level of nesting within a view hierarchy. It can be added by performing the following steps:
1. Make sure the following dependency is added to your module's (e.g. app's) build.gradle file under
dependencies:
compile 'com.android.support:appcompat-v7:25.3.1'
2. Set the theme for your app to one that does not have an ActionBar. To do that, edit your styles.xml file
under res/values, and set a Theme.AppCompat theme.
In this example we are using Theme.AppCompat.NoActionBar as parent of your AppTheme:
<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primaryDark</item>
<item name="colorAccent">@color/accent</item>
</style>
You can also use Theme.AppCompat.Light.NoActionBar or Theme.AppCompat.DayNight.NoActionBar, or any
other theme that does not inherently have an ActionBar
3. Add the Toolbar to your activity layout:
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"/>
Below the Toolbar you can add the rest of your layout.
4. In your Activity, set the Toolbar as the ActionBar for this Activity. Provided that you're using the
appcompat library and an AppCompatActivity, you would use the setSupportActionBar() method:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
//...
GoalKicker.com – Android™ Notes for Professionals 252
}
After performing the above steps, you can use the getSupportActionBar() method to manipulate the Toolbar that
is set as the ActionBar.
For example, you can set the title as shown below:
getSupportActionBar().setTitle("Activity Title");
For example, you can also set title and background color as shown below:
CharSequence title = "Your App Name";
SpannableString s = new SpannableString(title);
s.setSpan(new ForegroundColorSpan(Color.RED), 0, title.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
getSupportActionBar().setTitle(s);
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(Color.argb(128, 0, 0, 0)));
Section 37.2: Buttons styled with Material Design
The AppCompat Support Library defines several useful styles for Buttons, each of which extend a base
Widget.AppCompat.Button style that is applied to all buttons by default if you are using an AppCompat theme. This
style helps ensure that all buttons look the same by default following the Material Design specification.
In this case the accent color is pink.
1. Simple Button: @style/Widget.AppCompat.Button
<Button
style="@style/Widget.AppCompat.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/simple_button"/>
2. Colored Button: @style/Widget.AppCompat.Button.Colored
The Widget.AppCompat.Button.Colored style extends the Widget.AppCompat.Button style and applies
automatically the accent color you selected in your app theme.
<Button
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="match_parent"
GoalKicker.com – Android™ Notes for Professionals 253
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/colored_button"/>
If you want to customize the background color without changing the accent color in your main theme you can
create a custom theme (extending the ThemeOverlay theme) for your Button and assign it to the button's
android:theme attribute:
<Button
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:theme="@style/MyButtonTheme"/>
Define the theme in res/values/themes.xml:
<style name="MyButtonTheme" parent="ThemeOverlay.AppCompat.Light">
<item name="colorAccent">@color/my_color</item>
</style>
3. Borderless Button: @style/Widget.AppCompat.Button.Borderless
<Button
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/borderless_button"/>
4. Borderless Colored Button: @style/Widget.AppCompat.Button.Borderless.Colored
<Button
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/borderless_colored_button"/>
Section 37.3: Adding a FloatingActionButton (FAB)
In the material design, a Floating action button represents the primary action in an Activity.
They are distinguished by a circled icon floating above the UI and have motion behaviors that include morphing,
GoalKicker.com – Android™ Notes for Professionals 254
launching, and a transferring anchor point.
Make sure the following dependency is added to your app's build.gradle file under dependencies:
compile 'com.android.support:design:25.3.1'
Now add the FloatingActionButton to your layout file:
<android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/some_icon"/>
where the src attribute references the icon that should be used for the floating action.
The result should look something like this (presuming your accent color is Material Pink):
By default, the background color of your FloatingActionButton will be set to your theme's accent color. Also, note
that a FloatingActionButton requires a margin around it to work properly. The recommended margin for the
bottom is 16dp for phones and 24dp for tablets.
Here are properties which you can use to customize the FloatingActionButton further (assuming
xmlns:app="http://schemas.android.com/apk/res-auto is declared as namespace the top of your layout):
app:fabSize: Can be set to normal or mini to switch between a normal sized or a smaller version.
app:rippleColor: Sets the color of the ripple effect of your FloatingActionButton. Can be a color resource
or hex string.
app:elevation: Can be a string, integer, boolean, color value, floating point, dimension value.
app:useCompatPadding: Enable compat padding. Maybe a boolean value, such as true or false. Set to true
to use compat padding on api-21 and later, in order to maintain a consistent look with older api levels.
You can find more examples about FAB here.
Section 37.4: RippleDrawable
Ripple touch effect was introduced with material design in Android 5.0 (API level 21) and the animation is
implemented by the new RippleDrawable class.
Drawable that shows a ripple effect in response to state changes. The anchoring position of the ripple for
a given state may be specified by calling setHotspot(float x, float y) with the corresponding state
attribute identifier.
Version ≥ 5.0
In general, ripple effect for regular buttons works by default in API 21 and above, and for other touchable views,
it can be achieved by specifying:
GoalKicker.com – Android™ Notes for Professionals 255
android:background="?android:attr/selectableItemBackground">
for ripples contained within the view or:
android:background="?android:attr/selectableItemBackgroundBorderless"
for ripples that extend beyond the view's bounds.
For example, in the image below,
B1 is a button that does not have any background,
B2 is set up with android:background="android:attr/selectableItemBackground"
B3 is set up with android:background="android:attr/selectableItemBackgroundBorderless"
GoalKicker.com – Android™ Notes for Professionals 256
(Image courtesy: http://blog.csdn.net/a396901990/article/details/40187203 )
You can achieve the same in code using:
int[] attrs = new int[]{R.attr.selectableItemBackground};
TypedArray typedArray = getActivity().obtainStyledAttributes(attrs);
int backgroundResource = typedArray.getResourceId(0, 0);
GoalKicker.com – Android™ Notes for Professionals 257
myView.setBackgroundResource(backgroundResource);
Ripples can also be added to a view using the android:foreground attribute the same way as above. As the name
suggests, in case the ripple is added to the foreground, the ripple will show up above any view it is added to (e.g.
ImageView, a LinearLayout containing multiple views, etc).
If you want to customize the ripple effect into a view, you need to create a new XML file, inside the drawable
directory.
Here are few examples:
Example 1: An unbounded ripple
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#ffff0000" />
Example 2: Ripple with mask and background color
<ripple android:color="#7777777"
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/mask"
android:drawable="#ffff00" />
<item android:drawable="@android:color/white"/>
</ripple>
If there is view with a background already specified with a shape, corners and any other tags, to add a ripple to that
view use a mask layer and set the ripple as the background of the view.
Example:
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:attr/colorControlHighlight">
<item android:id="@android:id/mask">
<shape
android:shape="rectangle">
solid android:color="#000000"/>
<corners
android:radius="25dp"/>
</shape>
</item>
<item android:drawable="@drawable/rounded_corners" />
</ripple>
Example 3: Ripple on top a drawable resource
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#ff0000ff">
<item android:drawable="@drawable/my_drawable" />
</ripple>
Usage: To attach your ripple xml file to any view, set it as background as following (assuming your ripple file is
named my_ripple.xml):
<View
android:id="@+id/myViewId"
android:layout_width="wrap_content"
GoalKicker.com – Android™ Notes for Professionals 258
android:layout_height="wrap_content"
android:background="@drawable/my_ripple" />
Selector:
The ripple drawable can also be used in place of color state list selectors if your target version is v21 or above (you
can also place the ripple selector in the drawable-v21 folder):
<!-- /drawable/button.xml: -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/button_pressed"/>
<item android:drawable="@drawable/button_normal"/>
</selector>
<!--/drawable-v21/button.xml:-->
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:colorControlHighlight">
<item android:drawable="@drawable/button_normal" />
</ripple>
In this case, the color of the default state of your view would be white and the pressed state would show the ripple
drawable.
Point to note: Using ?android:colorControlHighlight will give the ripple the same color as the built-in ripples in
your app.
To change just the ripple color, you can customize the color android:colorControlHighlight in your theme like so:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar">
<item name="android:colorControlHighlight">@color/your_custom_color</item>
</style>
</resources>
and then use this theme in your activities, etc. The effect would be like the image below:
GoalKicker.com – Android™ Notes for Professionals 259
(Image courtesy: http://blog.csdn.net/a396901990/article/details/40187203 )
Section 37.5: Adding a TabLayout
TabLayout provides a horizontal layout to display tabs, and is commonly used in conjunction with a ViewPager.
Make sure the following dependency is added to your app's build.gradle file under dependencies:
GoalKicker.com – Android™ Notes for Professionals 260
compile 'com.android.support:design:25.3.1'
Now you can add items to a TabLayout in your layout using the TabItem class.
For example:
<android.support.design.widget.TabLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:id="@+id/tabLayout">
<android.support.design.widget.TabItem
android:text="@string/tab_text_1"
android:icon="@drawable/ic_tab_1"/>
<android.support.design.widget.TabItem
android:text="@string/tab_text_2"
android:icon="@drawable/ic_tab_2"/>
</android.support.design.widget.TabLayout>
Add an OnTabSelectedListener to be notified when a tab in the TabLayout is selected/unselected/reselected:
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabLayout);
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
int position = tab.getPosition();
// Switch to view for this tab
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
Tabs can also be added/removed from the TabLayout programmatically.
TabLayout.Tab tab = tabLayout.newTab();
tab.setText(R.string.tab_text_1);
tab.setIcon(R.drawable.ic_tab_1);
tabLayout.addTab(tab);
tabLayout.removeTab(tab);
tabLayout.removeTabAt(0);
tabLayout.removeAllTabs();
TabLayout has two modes, fixed and scrollable.
tabLayout.setTabMode(TabLayout.MODE_FIXED);
tabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
These can also be applied in XML:
GoalKicker.com – Android™ Notes for Professionals 261
<android.support.design.widget.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="fixed|scrollable" />
Note: the TabLayout modes are mutually exclusive, meaning only one can be active at a time.
The tab indicator color is the accent color defined for your Material Design theme.
You can override this color by defining a custom style in styles.xml and then applying the style to your TabLayout:
<style name="MyCustomTabLayoutStyle" parent="Widget.Design.TabLayout">
<item name="tabIndicatorColor">@color/your_color</item>
</style>
Then you can apply the style to the view using:
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
style="@style/MyCustomTabLayoutStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</android.support.design.widget.TabLayout>
Section 37.6: Bottom Sheets in Design Support Library
Bottom sheets slide up from the bottom of the screen to reveal more content.
They were added to the Android Support Library in v25.1.0 version and supports above all the versions.
Make sure the following dependency is added to your app's build.gradle file under dependencies:
compile 'com.android.support:design:25.3.1'
Persistent Bottom Sheets
You can achieve a Persistent Bottom Sheet attaching a BottomSheetBehavior to a child View of a
CoordinatorLayout:
<android.support.design.widget.CoordinatorLayout >
<!-- ..... -->
<LinearLayout
android:id="@+id/bottom_sheet"
android:elevation="4dp"
android:minHeight="120dp"
app:behavior_peekHeight="120dp"
...
app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
<!-- ..... -->
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
Then in your code you can create a reference using:
GoalKicker.com – Android™ Notes for Professionals 262
// The View with the BottomSheetBehavior
View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);
BottomSheetBehavior mBottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
You can set the state of your BottomSheetBehavior using the setState() method:
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
You can use one of these states:
STATE_COLLAPSED: this collapsed state is the default and shows just a portion of the layout along the bottom.
The height can be controlled with the app:behavior_peekHeight attribute (defaults to 0)
STATE_EXPANDED: the fully expanded state of the bottom sheet, where either the whole bottom sheet is visible
(if its height is less than the containing CoordinatorLayout) or the entire CoordinatorLayout is filled
STATE_HIDDEN: disabled by default (and enabled with the app:behavior_hideable attribute), enabling this
allows users to swipe down on the bottom sheet to completely hide the bottom sheet
Further to open or close the BottomSheet on click of a View of your choice, A Button let's say, here is how to toggle
the sheet behavior and update view.
mButton = (Button) findViewById(R.id.button_2);
//On Button click we monitor the state of the sheet
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mBottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
//If expanded then collapse it (setting in Peek mode).
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
mButton.setText(R.string.button2_hide);
} else if (mBottomSheetBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
//If Collapsed then hide it completely.
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
mButton.setText(R.string.button2);
} else if (mBottomSheetBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
//If hidden then Collapse or Expand, as the need be.
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
mButton.setText(R.string.button2_peek);
}
}
});
But BottomSheet behavior also has a feature where user can interact with the swipe UP or Down it with a DRAG
motion. In such a case, we might not be able to update the dependent View (like the button above) If the Sheet
state has changed. For that matter, you’d like to receive callbacks of state changes, hence you can add a
BottomSheetCallback to listen to user swipe events:
mBottomSheetBehavior.setBottomSheetCallback(new BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
// React to state change and notify views of the current state
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
// React to dragging events and animate views or transparency of dependent views
}
GoalKicker.com – Android™ Notes for Professionals 263
});
And if you only want your Bottom Sheet to be visible only in COLLAPSED and EXPANDED mode toggles and never
HIDE use:
mBottomSheetBehavior2.setHideable(false);
Bottom Sheet DialogFragment
You can also display a BottomSheetDialogFragment in place of a View in the bottom sheet. To do this, you first need
to create a new class that extends BottomSheetDialogFragment.
Within the setupDialog() method, you can inflate a new layout file and retrieve the BottomSheetBehavior of the
container view in your Activity. Once you have the behavior, you can create and associate a BottomSheetCallback
with it to dismiss the Fragment when the sheet is hidden.
public class BottomSheetDialogFragmentExample extends BottomSheetDialogFragment {
private BottomSheetBehavior.BottomSheetCallback mBottomSheetBehaviorCallback = new
BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
dismiss();
}
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
}
};
@Override
public void setupDialog(Dialog dialog, int style) {
super.setupDialog(dialog, style);
View contentView = View.inflate(getContext(), R.layout.fragment_bottom_sheet, null);
dialog.setContentView(contentView);
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) ((View)
contentView.getParent()).getLayoutParams();
CoordinatorLayout.Behavior behavior = params.getBehavior();
if( behavior != null && behavior instanceof BottomSheetBehavior ) {
((BottomSheetBehavior) behavior).setBottomSheetCallback(mBottomSheetBehaviorCallback);
}
}
}
Finally, you can call show() on an instance of your Fragment to display it in the bottom sheet.
BottomSheetDialogFragment bottomSheetDialogFragment = new BottomSheetDialogFragmentExample();
bottomSheetDialogFragment.show(getSupportFragmentManager(), bottomSheetDialogFragment.getTag());
You can find more details in the dedicated topic
GoalKicker.com – Android™ Notes for Professionals 264
Section 37.7: Apply an AppCompat theme
The AppCompat support library provides themes to build apps with the Material Design specification. A theme with
a parent of Theme.AppCompat is also required for an Activity to extend AppCompatActivity.
The first step is to customize your theme’s color palette to automatically colorize your app.
In your app's res/styles.xml you can define:
<!-- inherit from the AppCompat theme -->
<style name="AppTheme" parent="Theme.AppCompat">
<!-- your app branding color for the app bar -->
<item name="colorPrimary">#2196f3</item>
<!-- darker variant for the status bar and contextual app bars -->
<item name="colorPrimaryDark">#1976d2</item>
<!-- theme UI controls like checkboxes and text fields -->
<item name="colorAccent">#f44336</item>
</style>
Instead of Theme.AppCompat, which has a dark background, you can also use Theme.AppCompat.Light or
Theme.AppCompat.Light.DarkActionBar.
You can customize the theme with your own colours. Good choices are in the Material design specification colour
chart, and Material Palette. The "500" colours are good choices for primary (blue 500 in this example); choose "700"
of the same hue for the dark one; and an a shade from a different hue as the accent colour. The primary colour is
used for your app's toolbar and its entry in the overview (recent apps) screen, the darker variant to tint the status
bar, and the accent colour to highlight some controls.
After creating this theme, apply it to your app in the AndroidManifest.xml and also apply the theme to any
particular activity. This is useful for applying a AppTheme.NoActionBar theme, which lets you implement non-default
toolbar configurations.
<application android:theme="@style/AppTheme"
...>
<activity
android:name=".MainActivity"
android:theme="@style/AppTheme" />
</application>
You can also apply themes to individual Views using android:theme and a ThemeOverlay theme. For example with a
Toolbar:
<android.support.v7.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
or a Button:
<Button
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:theme="@style/MyButtonTheme"/>
GoalKicker.com – Android™ Notes for Professionals 265
<!-- res/values/themes.xml -->
<style name="MyButtonTheme" parent="ThemeOverlay.AppCompat.Light">
<item name="colorAccent">@color/my_color</item>
</style>
Section 37.8: Add a Snackbar
One of the main features in Material Design is the addition of a Snackbar, which in theory replaces the previous
Toast. As per the Android documentation:
Snackbars contain a single line of text directly related to the operation performed. They may contain a
text action, but no icons. Toasts are primarily used for system messaging. They also display at the bottom
of the screen, but may not be swiped off-screen.
Toasts can still be used in Android to display messages to users, however if you have decided to opt for material
design usage in your app, it is recommended that you actually use a snackbar. Instead of being displayed as an
overlay on your screen, a Snackbar pops from the bottom.
Here is how it is done:
Snackbar snackbar = Snackbar
.make(coordinatorLayout, "Here is your new Snackbar", Snackbar.LENGTH_LONG);
snackbar.show();
As for the length of time to show the Snackbar, we have the options similar to the ones offered by a Toast or we
could set a custom duration in milliseconds:
LENGTH_SHORT
LENGTH_LONG
LENGTH_INDEFINITE
setDuration() (since version 22.2.1)
You can also add dynamic features to your Snackbar such as ActionCallback or custom color. However do pay
attention to the design guideline offered by Android when customising a Snackbar.
GoalKicker.com – Android™ Notes for Professionals 266
Implementing the Snackbar has one limitation however. The parent layout of the view you are going to implement a
Snackbar in needs to be a CoordinatorLayout. This is so that the actual popup from the bottom can be made.
This is how to define a CoordinatorLayout in your layout xml file:
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
//any other widgets in your layout go here.
</android.support.design.widget.CoordinatorLayout>
The CoordinatorLayout then needs to be defined in your Activity's onCreate method, and then used when creating
the Snackbar itself.
For more information about about the Snackbar, please check the official documentation or the dedicated topic in
the documentation.
Section 37.9: Add a Navigation Drawer
Navigation Drawers are used to navigate to top-level destinations in an app.
Make sure that you have added design support library in your build.gradle file under dependencies:
dependencies {
// ...
compile 'com.android.support:design:25.3.1'
}
Next, add the DrawerLayout and NavigationView in your XML layout resource file.
The DrawerLayout is just a fancy container that allows the NavigationView, the actual navigation drawer, to slide
out from the left or right of the screen. Note: for mobile devices, the standard drawer size is 320dp.
<!-- res/layout/activity_main.xml -->
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/navigation_drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">
<! -- You can use "end" to open drawer from the right side -->
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
GoalKicker.com – Android™ Notes for Professionals 267
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.NavigationView
android:id="@+id/navigation_drawer"
android:layout_width="320dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/drawer_header"
app:menu="@menu/navigation_menu" />
</android.support.v4.widget.DrawerLayout>
Now, if you wish, create a header file that will serve as the top of your navigation drawer. This is used to give a
much more elegant look to the drawer.
<!-- res/layout/drawer_header.xml -->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="190dp">
<ImageView
android:id="@+id/header_image"
android:layout_width="140dp"
android:layout_height="120dp"
android:layout_centerInParent="true"
android:scaleType="centerCrop"
android:src="@drawable/image" />
<TextView
android:id="@+id/header_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/header_image"
android:text="User name"
android:textSize="20sp" />
</RelativeLayout>
It is referenced in the NavigationView tag in the app:headerLayout="@layout/drawer_header" attribute.
This app:headerLayout inflates the specified layout into the header automatically. This can alternatively be done at
runtime with:
// Lookup navigation view
NavigationView navigationView = (NavigationView) findViewById(R.id.navigation_drawer);
// Inflate the header view at runtime
View headerLayout = navigationView.inflateHeaderView(R.layout.drawer_header);
To automatically populate your navigation drawer with material design-compliant navigation items, create a menu
GoalKicker.com – Android™ Notes for Professionals 268
file and add items as needed. Note: while icons for items aren't required, they are suggested in the Material Design
specification.
It is referenced in the NavigationView tag in the app:menu="@menu/navigation_menu" attribute.
<!-- res/menu/menu_drawer.xml -->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_item_1"
android:title="Item #1"
android:icon="@drawable/ic_nav_1" />
<item
android:id="@+id/nav_item_2"
android:title="Item #2"
android:icon="@drawable/ic_nav_2" />
<item
android:id="@+id/nav_item_3"
android:title="Item #3"
android:icon="@drawable/ic_nav_3" />
<item
android:id="@+id/nav_item_4"
android:title="Item #4"
android:icon="@drawable/ic_nav_4" />
</menu>
To separate items into groups, put them into a <menu> nested in another <item> with an android:title attribute or
wrap them with the <group> tag.
Now that the layout is done, move on to the Activity code:
// Find the navigation view
NavigationView navigationView = (NavigationView) findViewById(R.id.navigation_drawer);
navigationView.setNavigationItemSelectedListener(new
NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(MenuItem item) {
// Get item ID to determine what to do on user click
int itemId = item.getItemId();
// Respond to Navigation Drawer selections with a new Intent
startActivity(new Intent(this, OtherActivity.class));
return true;
}
});
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.navigation_drawer_layout);
// Necessary for automatically animated navigation drawer upon open and close
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, "Open navigation drawer",
"Close navigation drawer");
// The two Strings are not displayed to the user, but be sure to put them into a separate
strings.xml file.
drawer.addDrawerListener(toggle);
toogle.syncState();
You can now do whatever you want in the header view of the NavigationView
View headerView = navigationView.getHeaderView();
TextView headerTextView = (TextView) headerview.findViewById(R.id.header_text_view);
ImageView headerImageView = (ImageView) headerview.findViewById(R.id.header_image);
// Set navigation header text
headerTextView.setText("User name");
// Set navigation header image
GoalKicker.com – Android™ Notes for Professionals 269
headerImageView.setImageResource(R.drawable.header_image);
The header view behaves like any other View, so once you use findViewById() and add some other Views to your
layout file, you can set the properties of anything in it.
You can find more details and examples in the dedicated topic.
Section 37.10: How to use TextInputLayout
Make sure the following dependency is added to your app's build.gradle file under dependencies:
compile 'com.android.support:design:25.3.1'
Show the hint from an EditText as a floating label when a value is entered.
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/form_username"/>
</android.support.design.widget.TextInputLayout>
For displaying the password display eye icon with TextInputLayout, we can make use of the following code:
<android.support.design.widget.TextInputLayout
android:id="@+id/input_layout_current_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordToggleEnabled="true">
<android.support.design.widget.TextInputEditText
android:id="@+id/current_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/current_password"
android:inputType="textPassword" />
</android.support.design.widget.TextInputLayout>
where app:passwordToggleEnabled="true" & android:inputType="textPassword" parameters are required.
app should use the namespace xmlns:app="http://schemas.android.com/apk/res-auto"
You can find more details and examples in the dedicated topic.
GoalKicker.com – Android™ Notes for Professionals 270
Chapter 38: Resources
Section 38.1: Define colors
Colors are usually stored in a resource file named colors.xml in the /res/values/ folder.
They are defined by <color> elements:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="blackOverlay">#66000000</color>
</resources>
Colors are represented by hexadecimal color values for each color channel (0 - FF) in one of the formats:
#RGB
#ARGB
#RRGGBB
#AARRGGBB
Legend
A - alpha channel - 0 value is fully transparent, FF value is opaque
R - red channel
G - green channel
B - blue channel
Defined colors can be used in XML with following syntax @color/name_of_the_color
For example:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/blackOverlay">
Using colors in code
These examples assume this is an Activity reference. A Context reference can be used in its place as well.
Version ≥ 1.6
int color = ContextCompat.getColor(this, R.color.black_overlay);
view.setBackgroundColor(color);
Version < 6.0
int color = this.getResources().getColor(this, R.color.black_overlay);
view.setBackgroundColor(color);
In above declaration colorPrimary, colorPrimaryDark and colorAccent are used to define Material design colors
that will be used in defining custom Android theme in styles.xml. They are automatically added when new project
is created with Android Studio.
GoalKicker.com – Android™ Notes for Professionals 271
Section 38.2: Color Transparency(Alpha) Level
Hex Opacity Values
------------------------------
| Alpha(%) | Hex Value |
------------------------------
| 100% | FF |
| 95% | F2 |
| 90% | E6 |
| 85% | D9 |
| 80% | CC |
| 75% | BF |
| 70% | B3 |
| 65% | A6 |
| 60% | 99 |
| 55% | 8C |
| 50% | 80 |
| 45% | 73 |
| 40% | 66 |
| 35% | 59 |
| 30% | 4D |
| 25% | 40 |
| 20% | 33 |
| 15% | 26 |
| 10% | 1A |
| 5% | 0D |
| 0% | 00 |
------------------------------
If you want to set 45% to red color.
<color name="red_with_alpha_45">#73FF0000</color>
hex value for red - #FF0000
You can add 73 for 45% opacity in prefix - #73FF0000
Section 38.3: Define String Plurals
To differentiate between plural and singular strings, you can define a plural in your strings.xml file and list the
different quantities, as shown in the example below:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="hello_people">
<item quantity="one">Hello to %d person</item>
<item quantity="other">Hello to %d people</item>
</plurals>
</resources>
This definition can be accessed from Java code by using the getQuantityString() method of the Resources class,
as shown in the following example:
getResources().getQuantityString(R.plurals.hello_people, 3, 3);
Here, the first parameter R.plurals.hello_people is the resource name. The second parameter (3 in this example)
GoalKicker.com – Android™ Notes for Professionals 272
is used to pick the correct quantity string. The third parameter (also 3 in this example) is the format argument that
will be used for substituting the format specifier %d.
Possible quantity values (listed in alphabetical order) are:
few
many
one
other
two
zero
It is important to note that not all locales support every denomination of quantity. For example, the Chinese
language does not have a concept of one item. English does not have a zero item, as it is grammatically the same as
other. Unsupported instances of quantity will be flagged by the IDE as Lint warnings, but won't cause complication
errors if they are used.
Section 38.4: Define strings
Strings are typically stored in the resource file strings.xml. They are defined using a <string> XML element.
The purpose of strings.xml is to allow internationalisation. You can define a strings.xml for each language iso code.
Thus when the system looks for the string 'app_name' it first checks the xml file corresponding to the current
language, and if it is not found, looks for the entry in the default strings.xml file. This means you can choose to only
localise some of your strings while not others.
/res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Hello World App</string>
<string name="hello_world">Hello World!</string>
</resources>
Once a string is defined in an XML resource file, it can be used by other parts of the app.
An app's XML project files can use a <string> element by referring to @string/string_name. For example, an app's
manifest (/manifests/AndroidManifest.xml) file includes the following line by default in Android Studio:
android:label="@string/app_name"
This tells android to look for a <string> resource called "app_name" to use as the name for the app when it is
installed or displayed in a launcher.
Another time you would use a <string> resource from an XML file in android would be in a layout file. For example,
the following represents a TextView which displays the hello_world string we defined earlier:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world"/>
You can also access <string> resources from the java portion of your app. To recall our same hello_world string
from above within an Activity class, use:
GoalKicker.com – Android™ Notes for Professionals 273
String helloWorld = getString(R.string.hello_world);
Section 38.5: Define dimensions
Dimensions are typically stored in a resource file names dimens.xml. They are defined using a <dimen> element.
res/values/dimens.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="small_padding">5dp</dimen>
<dimen name="medium_padding">10dp</dimen>
<dimen name="large_padding">20dp</dimen>
<dimen name="small_font">14sp</dimen>
<dimen name="medium_font">16sp</dimen>
<dimen name="large_font">20sp</dimen>
</resources>
You can use different units :
sp : Scale-independent Pixels. For fonts.
dp : Density-independent Pixels. For everything else.
pt : Points
px : Pixels
mm : Millimeters
im : Inches
Dimensions can now be referenced in XML with the syntax @dimen/name_of_the_dimension.
For example:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/large_padding">
</RelativeLayout>
Section 38.6: String formatting in strings.xml
Defining Strings in the strings.xml file also allows for string formatting. The only caveat is that the String will need to
be dealt with in code like below, versus simply attaching it to a layout.
<string name="welcome_trainer">Hello Pokémon Trainer, %1$s! You have caught %2$d Pokémon.</string>
String welcomePokemonTrainerText = getString(R.string.welcome_trainer, tranerName, pokemonCount);
In above example,
%1$s
'%' separates from normal characters,
'1' denotes first parameter,
'$' is used as separator between parameter number and type,
's' denotes string type ('d' is used for integer)
GoalKicker.com – Android™ Notes for Professionals 274
Note that getString() is a method of Context or Resources, i.e. you can use it directly within an Activity instance,
or else you may use getActivity().getString() or getContext().getString() respectively.
Section 38.7: Define integer array
In order to define an integer array write in a resources file
res/values/filename.xml
<integer-array name="integer_array_name">
<item>integer_value</item>
<item>@integer/integer_id</item>
</integer-array>
for example
res/values/arrays.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer-array name="fibo">
<item>@integer/zero</item>
<item>@integer/one</item>
<item>@integer/one</item>
<item>@integer/two</item>
<item>@integer/three</item>
<item>@integer/five</item>
</integer-array>
</resources>
and use it from java like
int[] values = getResources().getIntArray(R.array.fibo);
Log.i("TAG",Arrays.toString(values)));
Output
I/TAG: [0, 1, 1, 2, 3, 5]
Section 38.8: Define a color state list
Color state lists can be used as colors, but will change depending on the state of the view they are used for.
To define one, create a resource file in res/color/foo.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#888888" android:state_enabled="false"/>
<item android:color="@color/lightGray" android:state_selected="false"/>
<item android:color="@android:color/white" />
</selector>
Items are evaluated in the order they are defined, and the first item whose specified states match the current state
of the view is used. So it's a good practice to specify a catch-all at the end, without any state selectors specified.
Each item can either use a color literal, or reference a color defined somewhere else.
GoalKicker.com – Android™ Notes for Professionals 275
Section 38.9: 9 Patches
9 Patches are stretchable images in which the areas which can be stretched are defined by black markers on a
transparent border.
There is a great tutorial here.
Despite being so old, it's still so valuable and it helped many of us to deeply understand the 9 patch gear.
Unfortunately, recently that page has been put down for a while (it's currently up again).
Hence, the need to have a physical copy of that page for android developers on our reliable server/s.
Here it is.
A SIMPLE GUIDE TO 9-PATCH FOR ANDROID UI May 18, 2011
While I was working on my first Android app, I found 9-patch (aka 9.png) to be confusing and poorly documented.
After a little while, I finally picked up on how it works and decided to throw together something to help others figure
it out.
Basically, 9-patch uses png transparency to do an advanced form of 9-slice or scale9. The guides are straight, 1-pixel
black lines drawn on the edge of your image that define the scaling and fill of your image. By naming your image
file name.9.png, Android will recognize the 9.png format and use the black guides to scale and fill your bitmaps.
Here’s a basic guide map:
As you can see, you have guides on each side of your image. The TOP and LEFT guides are for scaling your image
(i.e. 9-slice), while the RIGHT and BOTTOM guides define the fill area.
The black guide lines are cut-off/removed from your image – they won’t show in the app. Guides must only be one
pixel wide, so if you want a 48×48 button, your png will actually be 50×50. Anything thicker than one pixel will
remain part of your image. (My examples have 4-pixel wide guides for better visibility. They should really be only 1-
pixel).
Your guides must be solid black (#000000). Even a slight difference in color (#000001) or alpha will cause it to fail
GoalKicker.com – Android™ Notes for Professionals 276
and stretch normally. This failure won’t be obvious either*, it fails silently! Yes. Really. Now you know.
Also you should keep in mind that remaining area of the one-pixel outline must be completely transparent. This
includes the four corners of the image – those should always be clear. This can be a bigger problem than you
realize. For example, if you scale an image in Photoshop it will add anti-aliased pixels which may include almostinvisible
pixels which will also cause it to fail*. If you must scale in Photoshop, use the Nearest Neighbor setting in
the Resample Image pulldown menu (at the bottom of the Image Size pop-up menu) to keep sharp edges on your
guides.
*(updated 1/2012) This is actually a “fix” in the latest dev kit. Previously it would manifest itself as all of your other
images and resources suddenly breaking, not the actually broken 9-patch image.
The TOP and LEFT guides are used to define the scalable portion of your image – LEFT for scaling height, TOP for
scaling width. Using a button image as an example, this means the button can stretch horizontally and vertically
within the black portion and everything else, such as the corners, will remain the same size. The allows you to have
buttons that can scale to any size and maintain a uniform look.
It’s important to note that 9-patch images don’t scale down – they only scale up. So it’s best to start as small as
possible.
Also, you can leave out portions in the middle of the scale line. So for example, if you have a button with a sharp
glossy edge across the middle, you can leave out a few pixels in the middle of the LEFT guide. The center horizontal
axis of your image won’t scale, just the parts above and below it, so your sharp gloss won’t get anti-aliased or fuzzy.
GoalKicker.com – Android™ Notes for Professionals 277
Fill area guides are optional and provide a way define the area for stuff like your text label. Fill determines how
much room there is within your image to place text, or an icon, or other things. 9-patch isn’t just for buttons, it
works for background images as well.
The above button & label example is exaggerated simply to explain the idea of fill – the label isn’t completely
accurate. To be honest, I haven’t experienced how Android does multi-line labels since a button label is usually a
single row of text.
Finally, here’s a good demonstration of how scale and fill guides can vary, such as a LinearLayout with a
background image & fully rounded sides:
With this example, the LEFT guide isn’t used but we’re still required to have a guide. The background image don’t
scale vertically; it just scales horizontally (based on the TOP guide). Looking at the fill guides, the RIGHT and
BOTTOM guides extend beyond where they meet the image’s curved edges. This allows me to place my round
buttons close to the edges of the background for a tight, fitted look.
So that’s it. 9-patch is super easy, once you get it. It’s not a perfect way to do scaling, but the fill-area and multi-line
scale-guides does offer more flexibility than traditional 9-slice and scale9. Give it a try and you’ll figure it out quickly.
GoalKicker.com – Android™ Notes for Professionals 278
Section 38.10: Getting resources without "deprecated"
warnings
Using the Android API 23 or higher, very often such situation can be seen:
This situation is caused by the structural change of the Android API regarding getting the resources.
Now the function:
public int getColor(@ColorRes int id, @Nullable Theme theme) throws NotFoundException
should be used. But the android.support.v4 library has another solution.
Add the following dependency to the build.gradle file:
com.android.support:support-v4:24.0.0
Then all methods from support library are available:
ContextCompat.getColor(context, R.color.colorPrimaryDark);
ContextCompat.getDrawable(context, R.drawable.btn_check);
ContextCompat.getColorStateList(context, R.color.colorPrimary);
DrawableCompat.setTint(drawable);
ContextCompat.getColor(context,R.color.colorPrimaryDark));
Moreover more methods from support library can be used:
ViewCompat.setElevation(textView, 1F);
ViewCompat.animate(textView);
TextViewCompat.setTextAppearance(textView, R.style.AppThemeTextStyle);
...
Section 38.11: Working with strings.xml file
A string resource provides text strings for your application with optional text styling and formatting. There are three
types of resources that can provide your application with strings:
String
XML resource that provides a single string.
Syntax:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="string_name">text_string</string>
</resources>
And to use this string in layout:
<TextView
android:layout_width="fill_parent"
GoalKicker.com – Android™ Notes for Professionals 279
android:layout_height="wrap_content"
android:text="@string/string_name" />
String Array
XML resource that provides an array of strings.
Syntax:
<resources>
<string-array name="planets_array">
<item>Mercury</item>
<item>Venus</item>
<item>Earth</item>
<item>Mars</item>
</string-array>
Usage
Resources res = getResources();
String[] planets = res.getStringArray(R.array.planets_array);
Quantity Strings (Plurals)
XML resource that carries different strings for pluralization.
Syntax:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals
name="plural_name">
<item
quantity=["zero" | "one" | "two" | "few" | "many" | "other"]
>text_string</item>
</plurals>
</resources>
Usage:
int count = getNumberOfsongsAvailable();
Resources res = getResources();
String songsFound = res.getQuantityString(R.plurals.plural_name, count, count);
Section 38.12: Define string array
In order to define a string array write in a resources file
res/values/filename.xml
<string-array name="string_array_name">
<item>text_string</item>
<item>@string/string_id</item>
</string-array>
for example
GoalKicker.com – Android™ Notes for Professionals 280
res/values/arrays.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="string_array_example">
<item>@string/app_name</item>
<item>@string/hello_world</item>
</string-array>
</resources>
and use it from java like
String[] strings = getResources().getStringArray(R.array.string_array_example;
Log.i("TAG",Arrays.toString(strings)));
Output
I/TAG: [HelloWorld, Hello World!]
Section 38.13: Define integers
Integers are typically stored in a resource file named integers.xml, but the file name can be chosen arbitrarily.
Each integer is defined by using an <integer> element, as shown in the following file:
res/values/integers.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="max">100</integer>
</resources>
Integers can now be referenced in XML with the syntax @integer/name_of_the_integer, as shown in the following
example:
<ProgressBar
android:layout_width="match_parent"
android:layout_height="match_parent"
android:max="@integer/max"/>
Section 38.14: Define a menu resource and use it inside
Activity/Fragment
Define a menu in res/menu
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/first_item_id"
android:orderInCategory="100"
android:title="@string/first_item_string"
android:icon="@drawable/first_item_icon"
app:showAsAction="ifRoom"/>
GoalKicker.com – Android™ Notes for Professionals 281
<item
android:id="@+id/second_item_id"
android:orderInCategory="110"
android:title="@string/second_item_string"
android:icon="@drawable/second_item_icon"
app:showAsAction="ifRoom"/>
</menu>
For more options of configuration refer to: Menu resource
Inside Activity:
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
///Override defining menu resource
inflater.inflate(R.menu.menu_resource_id, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
//Override for preparing items (setting visibility, change text, change icon...)
super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
//Override it for handling items
int menuItemId = item.getItemId();
switch (menuItemId) {
case: R.id.first_item_id
return true; //return true, if is handled
}
return super.onOptionsItemSelected(item);
}
For invoking the methods above during showing the view, call getActivity().invalidateOptionsMenu();
Inside Fragment one additional call is needed:
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
setHasOptionsMenu(true);
super.onCreateView(inflater, container, savedInstanceState);
}
GoalKicker.com – Android™ Notes for Professionals 282
Chapter 39: Data Binding Library
Section 39.1: Basic text field binding
Gradle (Module:app) Configuration
android {
....
dataBinding {
enabled = true
}
}
Data model
public class Item {
public String name;
public String description;
public Item(String name, String description) {
this.name = name;
this.description = description;
}
}
Layout XML
The first step is wrapping your layout in a <layout> tag, adding a <data> element, and adding a <variable>
element for your data model.
Then you can bind XML attributes to fields in the data model using @{model.fieldname}, where model is the
variable's name and fieldname is the field you want to access.
item_detail_activity.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="item" type="com.example.Item"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{item.name}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{item.description}"/>
</LinearLayout>
GoalKicker.com – Android™ Notes for Professionals 283
</layout>
For each XML layout file properly configured with bindings, the Android Gradle plugin generates a corresponding
class : bindings. Because we have a layout named item_detail_activity, the corresponding generated binding class is
called ItemDetailActivityBinding.
This binding can then be used in an Activity like so:
public class ItemDetailActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ItemDetailActivityBinding binding = DataBindingUtil.setContentView(this,
R.layout.item_detail_activity);
Item item = new Item("Example item", "This is an example item.");
binding.setItem(item);
}
}
Section 39.2: Built-in two-way Data Binding
Two-way Data-Binding supports the following attributes:
Element Properties
AbsListView android:selectedItemPosition
CalendarView android:date
CompoundButton android:checked
DatePicker
• android:year
• android:month
• android:day
EditText android:text
NumberPicker android:value
RadioGroup android:checkedButton
RatingBar android:rating
SeekBar android:progress
TabHost android:currentTab
TextView android:text
TimePicker • android:hour
• android:minute
ToggleButton android:checked
Switch android:checked
Usage
<layout ...>
<data>
<variable type="com.example.myapp.User" name="user"/>
</data>
<RelativeLayout ...>
<EditText android:text="@={user.firstName}" .../>
</RelativeLayout>
</layout>
Notice that the Binding expression @={} has an additional =, which is necessary for the two-way Binding. It is not
possible to use methods in two-way Binding expressions.
GoalKicker.com – Android™ Notes for Professionals 284
Section 39.3: Custom event using lambda expression
Define Interface
public interface ClickHandler {
public void onButtonClick(User user);
}
Create Model class
public class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Layout XML
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="handler"
type="com.example.ClickHandler"/>
<variable
name="user"
type="com.example.User"/>
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}"
android:onClick="@{() -> handler.onButtonClick(user)}"/>
</RelativeLayout>
</layout>
Activity code :
public class MainActivity extends Activity implements ClickHandler {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
GoalKicker.com – Android™ Notes for Professionals 285
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this,R.layout.activity_main);
binding.setUser(new User("DataBinding User"));
binding.setHandler(this);
}
@Override
public void onButtonClick(User user) {
Toast.makeText(MainActivity.this,"Welcome " + user.getName(),Toast.LENGTH_LONG).show();
}
}
For some view listener which is not available in xml code but can be set in Java code, it can be bind with custom
binding.
Custom class
public class BindingUtil {
@BindingAdapter({"bind:autoAdapter"})
public static void setAdapter(AutoCompleteTextView view, ArrayAdapter<String> pArrayAdapter) {
view.setAdapter(pArrayAdapter);
}
@BindingAdapter({"bind:onKeyListener"})
public static void setOnKeyListener(AutoCompleteTextView view , View.OnKeyListener
pOnKeyListener)
{
view.setOnKeyListener(pOnKeyListener);
}
}
Handler class
public class Handler extends BaseObservable {
private ArrayAdapter<String> roleAdapter;
public ArrayAdapter<String> getRoleAdapter() {
return roleAdapter;
}
public void setRoleAdapter(ArrayAdapter<String> pRoleAdapter) {
roleAdapter = pRoleAdapter;
}
}
XML
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/tools" >
<data>
<variable
name="handler"
type="com.example.Handler" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
GoalKicker.com – Android™ Notes for Professionals 286
<AutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
bind:autoAdapter="@{handler.roleAdapter}" />
</LinearLayout>
</layout>
Section 39.4: Default value in Data Binding
The Preview pane displays default values for data binding expressions if provided.
For example :
android:layout_height="@{@dimen/main_layout_height, default=wrap_content}"
It will take wrap_content while designing and will act as a wrap_content in preview pane.
Another example is
android:text="@{user.name, default=`Preview Text`}"
It will display Preview Text in preview pane but when you run it in device/emulator actual text binded to it will be
displayed
Section 39.5: Databinding in Dialog
public void doSomething() {
DialogTestBinding binding = DataBindingUtil
.inflate(LayoutInflater.from(context), R.layout.dialog_test, null, false);
Dialog dialog = new Dialog(context);
dialog.setContentView(binding.getRoot());
dialog.show();
}
Section 39.6: Binding with an accessor method
If your model has private methods, the databinding library still allows you to access them in your view without
using the full name of the method.
Data model
public class Item {
private String name;
public String getName() {
return name;
}
}
Layout XML
GoalKicker.com – Android™ Notes for Professionals 287
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="item" type="com.example.Item"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Since the "name" field is private on our data model,
this binding will utilize the public getName() method instead. -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{item.name}"/>
</LinearLayout>
</layout>
Section 39.7: Pass widget as reference in BindingAdapter
Layout XML
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/img"
android:layout_width="match_parent"
android:layout_height="100dp"
app:imageUrl="@{url}"
app:progressbar="@{progressBar}"/>
</LinearLayout>
</layout>
BindingAdapter method
@BindingAdapter({"imageUrl","progressbar"})
public static void loadImage(ImageView view, String imageUrl, ProgressBar progressBar){
Glide.with(view.getContext()).load(imageUrl)
.listener(new RequestListener<String, GlideDrawable>() {
@Override
public boolean onException(Exception e, String model, Target<GlideDrawable>
GoalKicker.com – Android™ Notes for Professionals 288
target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(GlideDrawable resource, String model,
Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
progressBar.setVisibility(View.GONE);
return false;
}
}).into(view);
}
Section 39.8: Click listener with Binding
Create interface for clickHandler
public interface ClickHandler {
public void onButtonClick(View v);
}
Layout XML
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="handler"
type="com.example.ClickHandler"/>
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="click me"
android:onClick="@{handler.onButtonClick}"/>
</RelativeLayout>
</layout>
Handle event in your Activity
public class MainActivity extends Activity implements ClickHandler {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this,R.layout.activity_main);
binding.setHandler(this);
}
@Override
public void onButtonClick(View v) {
Toast.makeText(context,"Button clicked",Toast.LENGTH_LONG).show();
GoalKicker.com – Android™ Notes for Professionals 289
}
}
Section 39.9: Data binding in RecyclerView Adapter
It's also possible to use data binding within your RecyclerView Adapter.
Data model
public class Item {
private String name;
public String getName() {
return name;
}
}
XML Layout
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{item.name}"/>
Adapter class
public class ListItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Activity host;
private List<Item> items;
public ListItemAdapter(Activity activity, List<Item> items) {
this.host = activity;
this.items = items;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// inflate layout and retrieve binding
ListItemBinding binding = DataBindingUtil.inflate(host.getLayoutInflater(),
R.layout.list_item, parent, false);
return new ItemViewHolder(binding);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
Item item = items.get(position);
ItemViewHolder itemViewHolder = (ItemViewHolder)holder;
itemViewHolder.bindItem(item);
}
@Override
public int getItemCount() {
return items.size();
}
private static class ItemViewHolder extends RecyclerView.ViewHolder {
ListItemBinding binding;
GoalKicker.com – Android™ Notes for Professionals 290
ItemViewHolder(ListItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
void bindItem(Item item) {
binding.setItem(item);
binding.executePendingBindings();
}
}
}
Section 39.10: Databinding in Fragment
Data Model
public class Item {
private String name;
public String getName() {
return name;
}
public void setName(String name){
this.name = name;
}
}
Layout XML
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="item" type="com.example.Item"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{item.name}"/>
</LinearLayout>
</layout>
Fragment
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle
savedInstanceState) {
FragmentTest binding = DataBindingUtil.inflate(inflater, R.layout.fragment_test, container,
false);
Item item = new Item();
item.setName("Thomas");
GoalKicker.com – Android™ Notes for Professionals 291
binding.setItem(item);
return binding.getRoot();
}
Section 39.11: DataBinding with custom variables(int,boolean)
Sometimes we need to perform basic operations like hide/show view based on single value, for that single variable
we cannot create model or it is not good practice to create model for that. DataBinding supports basic datatypes to
perform those oprations.
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View" />
<variable
name="selected"
type="Boolean" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World"
android:visibility="@{selected ? View.VISIBLE : View.GONE}" />
</RelativeLayout>
</layout>
and set its value from java class.
binding.setSelected(true);
Section 39.12: Referencing classes
Data model
public class Item {
private String name;
public String getName() {
return name;
}
}
Layout XML
You must import referenced classes, just as you would in Java.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
GoalKicker.com – Android™ Notes for Professionals 292
<import type="android.view.View"/>
<variable name="item" type="com.example.Item"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- We reference the View class to set the visibility of this TextView -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{item.name}"
android:visibility="@{item.name == null ? View.VISIBLE : View.GONE"/>
</LinearLayout>
</layout>
Note: The package java.lang.* is imported automatically by the system. (The same is made by JVM for Java)
GoalKicker.com – Android™ Notes for Professionals 293
Chapter 40: SharedPreferences
Parameter Details
key
A non-null String identifying the parameter. It can contain whitespace or non-printables. This is only
used inside your app (and in the XML file), so it doesn't have to be namespaced, but it's a good idea to
have it as a constant in your source code. Don't localize it.
defValue
All the get functions take a default value, which is returned if the given key is not present in the
SharedPreferences. It's not returned if the key is present but the value has the wrong type: in that
case you get a ClassCastException.
SharedPreferences provide a way to save data to disk in the form of key-value pairs.
Section 40.1: Implementing a Settings screen using
SharedPreferences
One use of SharedPreferences is to implement a "Settings" screen in your app, where the user can set their
preferences / options. Like this:
A PreferenceScreen saves user preferences in SharedPreferences. To create a PreferenceScreen, you need a few
things:
An XML file to define the available options:
This goes in /res/xml/preferences.xml, and for the above settings screen, it looks like this:
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="General options">
<CheckBoxPreference
android:key = "silent_mode"
android:defaultValue="false"
android:title="Silent Mode"
GoalKicker.com – Android™ Notes for Professionals 294
android:summary="Mute all sounds from this app" />
<SwitchPreference
android:key="awesome_mode"
android:defaultValue="false"
android:switchTextOn="Yes"
android:switchTextOff="No"
android:title="Awesome mode™"
android:summary="Enable the Awesome Mode™ feature"/>
<EditTextPreference
android:key="custom_storage"
android:defaultValue="/sdcard/data/"
android:title="Custom storage location"
android:summary="Enter the directory path where you want data to be saved. If it does
not exist, it will be created."
android:dialogTitle="Enter directory path (eg. /sdcard/data/ )"/>
</PreferenceCategory>
</PreferenceScreen>
This defines the available options in the settings screen. There are many other types of Preference listed in the
Android Developers documentation on the Preference Class.
Next, we need an Activity to host our Preferences user interface. In this case, it's quite short, and looks like this:
package com.example.preferences;
import android.preference.PreferenceActivity;
import android.os.Bundle;
public class PreferencesActivity extends PreferenceActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
}
}
It extends PreferenceActivity, and provides the user interface for the preferences screen. It can be started just
like a normal activity, in this case, with something like:
Intent i = new Intent(this, PreferencesActivity.class);
startActivity(i);
Don't forget to add PreferencesActivity to your AndroidManifest.xml.
Getting the values of the preferences inside your app is quite simple, just call setDefaultValues() first, in order
to set the default values defined in your XML, and then get the default SharedPreferences. An example:
//set the default values we defined in the XML
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
//get the values of the settings options
boolean silentMode = preferences.getBoolean("silent_mode", false);
boolean awesomeMode = preferences.getBoolean("awesome_mode", false);
String customStorage = preferences.getString("custom_storage", "");
GoalKicker.com – Android™ Notes for Professionals 295
Section 40.2: Commit vs. Apply
The editor.apply() method is asynchronous, while editor.commit() is synchronous.
Obviously, you should call either apply() or commit().
Version ≥ 2.3
SharedPreferences settings = getSharedPreferences(PREFS_FILE, MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean(PREF_CONST, true);
// This will asynchronously save the shared preferences without holding the current thread.
editor.apply();
SharedPreferences settings = getSharedPreferences(PREFS_FILE, MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean(PREF_CONST, true);
// This will synchronously save the shared preferences while holding the current thread until done
and returning a success flag.
boolean result = editor.commit();
apply() was added in 2.3 (API 9), it commits without returning a boolean indicating success or failure.
commit() returns true if the save works, false otherwise.
apply() was added as the Android dev team noticed that almost no one took notice of the return value, so apply is
faster as it is asynchronous.
Unlike commit(), which writes its preferences out to persistent storage synchronously, apply() commits its changes
to the in-memory SharedPreferences immediately but starts an asynchronous commit to disk and you won't be
notified of any failures. If another editor on this SharedPreferences does a regular commit() while a apply() is still
outstanding, the commit() will block until all async commits(apply) are completed as well as any other sync commits
that may be pending.
Section 40.3: Read and write values to SharedPreferences
public class MyActivity extends Activity {
private static final String PREFS_FILE = "NameOfYourPreferenceFile";
// PREFS_MODE defines which apps can access the file
private static final int PREFS_MODE = Context.MODE_PRIVATE;
// you can use live template "key" for quickly creating keys
private static final String KEY_BOOLEAN = "KEY_FOR_YOUR_BOOLEAN";
private static final String KEY_STRING = "KEY_FOR_YOUR_STRING";
private static final String KEY_FLOAT = "KEY_FOR_YOUR_FLOAT";
private static final String KEY_INT = "KEY_FOR_YOUR_INT";
private static final String KEY_LONG = "KEY_FOR_YOUR_LONG";
@Override
protected void onStart() {
super.onStart();
// Get the saved flag (or default value if it hasn't been saved yet)
SharedPreferences settings = getSharedPreferences(PREFS_FILE, PREFS_MODE);
// read a boolean value (default false)
boolean booleanVal = settings.getBoolean(KEY_BOOLEAN, false);
// read an int value (Default 0)
int intVal = settings.getInt(KEY_INT, 0);
// read a string value (default "my string")
String str = settings.getString(KEY_STRING, "my string");
GoalKicker.com – Android™ Notes for Professionals 296
// read a long value (default 123456)
long longVal = settings.getLong(KEY_LONG, 123456);
// read a float value (default 3.14f)
float floatVal = settings.getFloat(KEY_FLOAT, 3.14f);
}
@Override
protected void onStop() {
super.onStop();
// Save the flag
SharedPreferences settings = getSharedPreferences(PREFS_FILE, PREFS_MODE);
SharedPreferences.Editor editor = settings.edit();
// write a boolean value
editor.putBoolean(KEY_BOOLEAN, true);
// write an integer value
editor.putInt(KEY_INT, 123);
// write a string
editor.putString(KEY_STRING, "string value");
// write a long value
editor.putLong(KEY_LONG, 456876451);
// write a float value
editor.putFloat(KEY_FLOAT, 1.51f);
editor.apply();
}
}
getSharedPreferences() is a method from the Context class — which Activity extends. If you need to access the
getSharedPreferences() method from other classes, you can use context.getSharedPreferences() with a
Context Object reference from an Activity, View, or Application.
Section 40.4: Retrieve all stored entries from a particular
SharedPreferences file
The getAll() method retrieves all values from the preferences. We can use it, for instance, to log the current
content of the SharedPreferences:
private static final String PREFS_FILE = "MyPrefs";
public static void logSharedPreferences(final Context context) {
SharedPreferences sharedPreferences = context.getSharedPreferences(PREFS_FILE,
Context.MODE_PRIVATE);
Map<String, ?> allEntries = sharedPreferences.getAll();
for (Map.Entry<String, ?> entry : allEntries.entrySet()) {
final String key = entry.getKey();
final Object value = entry.getValue();
Log.d("map values", key + ": " + value);
}
}
The documentation warns you about modifying the Collection returned by getAll:
Note that you must not modify the collection returned by this method, or alter any of its contents. The
consistency of your stored data is not guaranteed if you do.
GoalKicker.com – Android™ Notes for Professionals 297
Section 40.5: Reading and writing data to SharedPreferences
with Singleton
SharedPreferences Manager (Singleton) class to read and write all types of data.
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import com.google.gson.Gson;
import java.lang.reflect.Type;
/**
* Singleton Class for accessing SharedPreferences,
* should be initialized once in the beginning by any application component using static
* method initialize(applicationContext)
*/
public class SharedPrefsManager {
private static final String TAG = SharedPrefsManager.class.getName();
private SharedPreferences prefs;
private static SharedPrefsManager uniqueInstance;
public static final String PREF_NAME = "com.example.app";
private SharedPrefsManager(Context appContext) {
prefs = appContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
}
/**
* Throws IllegalStateException if this class is not initialized
*
* @return unique SharedPrefsManager instance
*/
public static SharedPrefsManager getInstance() {
if (uniqueInstance == null) {
throw new IllegalStateException(
"SharedPrefsManager is not initialized, call initialize(applicationContext) " +
"static method first");
}
return uniqueInstance;
}
/**
* Initialize this class using application Context,
* should be called once in the beginning by any application Component
*
* @param appContext application context
*/
public static void initialize(Context appContext) {
if (appContext == null) {
throw new NullPointerException("Provided application context is null");
}
if (uniqueInstance == null) {
synchronized (SharedPrefsManager.class) {
if (uniqueInstance == null) {
uniqueInstance = new SharedPrefsManager(appContext);
}
}
}
}
GoalKicker.com – Android™ Notes for Professionals 298
private SharedPreferences getPrefs() {
return prefs;
}
/**
* Clears all data in SharedPreferences
*/
public void clearPrefs() {
SharedPreferences.Editor editor = getPrefs().edit();
editor.clear();
editor.commit();
}
public void removeKey(String key) {
getPrefs().edit().remove(key).commit();
}
public boolean containsKey(String key) {
return getPrefs().contains(key);
}
public String getString(String key, String defValue) {
return getPrefs().getString(key, defValue);
}
public String getString(String key) {
return getString(key, null);
}
public void setString(String key, String value) {
SharedPreferences.Editor editor = getPrefs().edit();
editor.putString(key, value);
editor.apply();
}
public int getInt(String key, int defValue) {
return getPrefs().getInt(key, defValue);
}
public int getInt(String key) {
return getInt(key, 0);
}
public void setInt(String key, int value) {
SharedPreferences.Editor editor = getPrefs().edit();
editor.putInt(key, value);
editor.apply();
}
public long getLong(String key, long defValue) {
return getPrefs().getLong(key, defValue);
}
public long getLong(String key) {
return getLong(key, 0L);
}
public void setLong(String key, long value) {
SharedPreferences.Editor editor = getPrefs().edit();
editor.putLong(key, value);
editor.apply();
}
GoalKicker.com – Android™ Notes for Professionals 299
public boolean getBoolean(String key, boolean defValue) {
return getPrefs().getBoolean(key, defValue);
}
public boolean getBoolean(String key) {
return getBoolean(key, false);
}
public void setBoolean(String key, boolean value) {
SharedPreferences.Editor editor = getPrefs().edit();
editor.putBoolean(key, value);
editor.apply();
}
public boolean getFloat(String key) {
return getFloat(key, 0f);
}
public boolean getFloat(String key, float defValue) {
return getFloat(key, defValue);
}
public void setFloat(String key, Float value) {
SharedPreferences.Editor editor = getPrefs().edit();
editor.putFloat(key, value);
editor.apply();
}
/**
* Persists an Object in prefs at the specified key, class of given Object must implement Model
* interface
*
* @param key String
* @param modelObject Object to persist
* @param <M> Generic for Object
*/
public <M extends Model> void setObject(String key, M modelObject) {
String value = createJSONStringFromObject(modelObject);
SharedPreferences.Editor editor = getPrefs().edit();
editor.putString(key, value);
editor.apply();
}
/**
* Fetches the previously stored Object of given Class from prefs
*
* @param key String
* @param classOfModelObject Class of persisted Object
* @param <M> Generic for Object
* @return Object of given class
*/
public <M extends Model> M getObject(String key, Class<M> classOfModelObject) {
String jsonData = getPrefs().getString(key, null);
if (null != jsonData) {
try {
Gson gson = new Gson();
M customObject = gson.fromJson(jsonData, classOfModelObject);
return customObject;
} catch (ClassCastException cce) {
Log.d(TAG, "Cannot convert string obtained from prefs into collection of type " +
classOfModelObject.getName() + "\n" + cce.getMessage());
}
GoalKicker.com – Android™ Notes for Professionals 300
}
return null;
}
/**
* Persists a Collection object in prefs at the specified key
*
* @param key String
* @param dataCollection Collection Object
* @param <C> Generic for Collection object
*/
public <C> void setCollection(String key, C dataCollection) {
SharedPreferences.Editor editor = getPrefs().edit();
String value = createJSONStringFromObject(dataCollection);
editor.putString(key, value);
editor.apply();
}
/**
* Fetches the previously stored Collection Object of given type from prefs
*
* @param key String
* @param typeOfC Type of Collection Object
* @param <C> Generic for Collection Object
* @return Collection Object which can be casted
*/
public <C> C getCollection(String key, Type typeOfC) {
String jsonData = getPrefs().getString(key, null);
if (null != jsonData) {
try {
Gson gson = new Gson();
C arrFromPrefs = gson.fromJson(jsonData, typeOfC);
return arrFromPrefs;
} catch (ClassCastException cce) {
Log.d(TAG, "Cannot convert string obtained from prefs into collection of type " +
typeOfC.toString() + "\n" + cce.getMessage());
}
}
return null;
}
public void registerPrefsListener(SharedPreferences.OnSharedPreferenceChangeListener listener)
{
getPrefs().registerOnSharedPreferenceChangeListener(listener);
}
public void unregisterPrefsListener(
SharedPreferences.OnSharedPreferenceChangeListener listener) {
getPrefs().unregisterOnSharedPreferenceChangeListener(listener);
}
public SharedPreferences.Editor getEditor() {
return getPrefs().edit();
}
private static String createJSONStringFromObject(Object object) {
Gson gson = new Gson();
return gson.toJson(object);
}
}
GoalKicker.com – Android™ Notes for Professionals 301
Model interface which is implemented by classes going to Gson to avoid proguard obfuscation.
public interface Model {
}
Proguard rules for Model interface:
-keep interface com.example.app.Model
-keep class * implements com.example.app.Model { *;}
Section 40.6: getPreferences(int) VS
getSharedPreferences(String, int)
getPreferences(int)
returns the preferences saved by Activity's class name as described in the docs :
Retrieve a SharedPreferences object for accessing preferences that are private to this activity. This simply
calls the underlying getSharedPreferences(String, int) method by passing in this activity's class name as
the preferences name.
While using getSharedPreferences (String name, int mode) method returns the prefs saved under the given name. As
in the docs :
Retrieve and hold the contents of the preferences file 'name', returning a SharedPreferences through
which you can retrieve and modify its values.
So if the value being saved in the SharedPreferences has to be used across the app, one should use
getSharedPreferences (String name, int mode) with a fixed name. As, using getPreferences(int)
returns/saves the preferences belonging to the Activity calling it.
Section 40.7: Listening for SharedPreferences changes
SharedPreferences sharedPreferences = ...;
sharedPreferences.registerOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener);
private final SharedPreferences.OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener
= new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
//TODO
}
}
Please note:
The listener will fire only if value was added or changed, setting the same value won't call it;
The listener needs to be saved in a member variable and NOT with an anonymous class, because
registerOnSharedPreferenceChangeListener stores it with a weak reference, so it would be garbage
collected;
GoalKicker.com – Android™ Notes for Professionals 302
Instead of using a member variable, it can also be directly implemented by the class and then call
registerOnSharedPreferenceChangeListener(this);
Remember to unregister the listener when it is no more required using
unregisterOnSharedPreferenceChangeListener.
Section 40.8: Store, Retrieve, Remove and Clear Data from
SharedPreferences
Create SharedPreferences BuyyaPref
SharedPreferences pref = getApplicationContext().getSharedPreferences("BuyyaPref", MODE_PRIVATE);
Editor editor = pref.edit();
Storing data as KEY/VALUE pair
editor.putBoolean("key_name1", true); // Saving boolean - true/false
editor.putInt("key_name2", 10); // Saving integer
editor.putFloat("key_name3", 10.1f); // Saving float
editor.putLong("key_name4", 1000); // Saving long
editor.putString("key_name5", "MyString"); // Saving string
// Save the changes in SharedPreferences
editor.commit(); // commit changes
Get SharedPreferences data
If value for key not exist then return second param value(In this case null, this is like default value)
pref.getBoolean("key_name1", null); // getting boolean
pref.getInt("key_name2", null); // getting Integer
pref.getFloat("key_name3", null); // getting Float
pref.getLong("key_name4", null); // getting Long
pref.getString("key_name5", null); // getting String
Deleting Key value from SharedPreferences
editor.remove("key_name3"); // will delete key key_name3
editor.remove("key_name4"); // will delete key key_name4
// Save the changes in SharedPreferences
editor.commit(); // commit changes
Clear all data from SharedPreferences
editor.clear();
editor.commit(); // commit changes
Section 40.9: Add filter for EditTextPreference
Create this class :
public class InputFilterMinMax implements InputFilter {
private int min, max;
public InputFilterMinMax(int min, int max) {
GoalKicker.com – Android™ Notes for Professionals 303
this.min = min;
this.max = max;
}
public InputFilterMinMax(String min, String max) {
this.min = Integer.parseInt(min);
this.max = Integer.parseInt(max);
}
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
int dend) {
try {
int input = Integer.parseInt(dest.toString() + source.toString());
if (isInRange(min, max, input))
return null;
} catch (NumberFormatException nfe) { }
return "";
}
private boolean isInRange(int a, int b, int c) {
return b > a ? c >= a && c <= b : c >= b && c <= a;
}
}
Use :
EditText compressPic = ((EditTextPreference)
findPreference(getString("pref_key_compress_pic"))).getEditText();
compressPic.setFilters(new InputFilter[]{ new InputFilterMinMax(1, 100) });
Section 40.10: Supported data types in SharedPreferences
SharedPreferences allows you to store primitive data types only (boolean, float, long, int, String, and string
set). You cannot store more complex objects in SharedPreferences, and as such is really meant to be a place to
store user settings or similar, it's not meant to be a database to keep user data (like saving a todo list a user made
for example).
To store something in SharedPreferences you use a Key and a Value. The Key is how you can reference what you
stored later and the Value data you want to store.
String keyToUseToFindLater = "High Score";
int newHighScore = 12938;
//getting SharedPreferences & Editor objects
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
//saving an int in the SharedPreferences file
editor.putInt(keyToUseToFindLater, newHighScore);
editor.commit();
Section 40.11: Dierent ways of instantiating an object of
SharedPreferences
You can access SharedPreferences in several ways:
Get the default SharedPreferences file:
GoalKicker.com – Android™ Notes for Professionals 304
import android.preference.PreferenceManager;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
Get a specific SharedPreferences file:
public static final String PREF_FILE_NAME = "PrefFile";
SharedPreferences prefs = getSharedPreferences(PREF_FILE_NAME, MODE_PRIVATE);
Get SharedPreferences from another app:
// Note that the other app must declare prefs as MODE_WORLD_WRITEABLE
final ArrayList<HashMap<String,String>> LIST = new ArrayList<HashMap<String,String>>();
Context contextOtherApp = createPackageContext("com.otherapp", Context.MODE_WORLD_WRITEABLE);
SharedPreferences prefs = contextOtherApp.getSharedPreferences("pref_file_name",
Context.MODE_WORLD_READABLE);
Section 40.12: Removing keys
private static final String MY_PREF = "MyPref";
// ...
SharedPreferences prefs = ...;
// ...
SharedPreferences.Editor editor = prefs.edit();
editor.putString(MY_PREF, "value");
editor.remove(MY_PREF);
editor.apply();
After the apply(), prefs contains "key" -> "value", in addition to whatever it contained already. Even though it looks
like I added "key" and then removed it, the remove actually happens first. The changes in the Editor are all applied
in one go, not in the order you added them. All removes happen before all puts.
Section 40.13: Support pre-Honeycomb with StringSet
Here's the utility class:
public class SharedPreferencesCompat {
public static void putStringSet(SharedPreferences.Editor editor, String key, Set<String>
values) {
if (Build.VERSION.SDK_INT >= 11) {
while (true) {
try {
editor.putStringSet(key, values).apply();
break;
} catch (ClassCastException ex) {
// Clear stale JSON string from before system upgrade
editor.remove(key);
}
}
} else putStringSetToJson(editor, key, values);
}
public static Set<String> getStringSet(SharedPreferences prefs, String key, Set<String>
defaultReturnValue) {
GoalKicker.com – Android™ Notes for Professionals 305
if (Build.VERSION.SDK_INT >= 11) {
try {
return prefs.getStringSet(key, defaultReturnValue);
} catch (ClassCastException ex) {
// If user upgraded from Gingerbread to something higher read the stale JSON string
return getStringSetFromJson(prefs, key, defaultReturnValue);
}
} else return getStringSetFromJson(prefs, key, defaultReturnValue);
}
private static Set<String> getStringSetFromJson(SharedPreferences prefs, String key,
Set<String> defaultReturnValue) {
final String input = prefs.getString(key, null);
if (input == null) return defaultReturnValue;
try {
HashSet<String> set = new HashSet<>();
JSONArray json = new JSONArray(input);
for (int i = 0, size = json.length(); i < size; i++) {
String value = json.getString(i);
set.add(value);
}
return set;
} catch (JSONException e) {
e.printStackTrace();
return defaultReturnValue;
}
}
private static void putStringSetToJson(SharedPreferences.Editor editor, String key, Set<String>
values) {
JSONArray json = new JSONArray(values);
if (Build.VERSION.SDK_INT >= 9)
editor.putString(key, json.toString()).apply();
else
editor.putString(key, json.toString()).commit();
}
private SharedPreferencesCompat() {}
}
An example to save preferences as StringSet data type is:
Set<String> sets = new HashSet<>();
sets.add("John");
sets.add("Nicko");
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferencesCompat.putStringSet(preferences.edit(), "pref_people", sets);
To retrieve them back:
Set<String> people = SharedPreferencesCompat.getStringSet(preferences, "pref_people", new
HashSet<String>());
Reference: Android Support Preference
GoalKicker.com – Android™ Notes for Professionals 306
Chapter 41: Intent
Parameter Details
intent The intent to start
requestCode Unique number to identify the request
options Additional options for how the Activity should be started
name The name of the extra data
value The value of the extra data
CHOOSE_CONTACT_REQUEST_CODE the code of the request, to identify it on onActivityResult method
action Any action to perform via this intent, ex: Intent.ACTION_VIEW
uri data uri to be used by intent to perform specified action
packageContext Context to use to initialize the Intent
cls Class to be used by this intent
An Intent is a small message passed around the Android system. This message may hold information about our
intention to perform a task.
It is basically a passive data structure holding an abstract description of an action to be performed.
Section 41.1: Getting a result from another Activity
By using startActivityForResult(Intent intent, int requestCode) you can start another Activity and then
receive a result from that Activity in the onActivityResult(int requestCode, int resultCode, Intent data)
method. The result will be returned as an Intent. An intent can contain data via a Bundle
In this example MainActivity will start a DetailActivity and then expect a result from it. Each request type
should have its own int request code, so that in the overridden onActivityResult(int requestCode, int
resultCode, Intent data) method in MainActivity , it can be determined which request to process by comparing
values of requestCode and REQUEST_CODE_EXAMPLE (though in this example, there is only one).
MainActivity:
public class MainActivity extends Activity {
// Use a unique request code for each use case
private static final int REQUEST_CODE_EXAMPLE = 0x9345;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Create a new instance of Intent to start DetailActivity
final Intent intent = new Intent(this, DetailActivity.class);
// Start DetailActivity with the request code
startActivityForResult(intent, REQUEST_CODE_EXAMPLE);
}
// onActivityResult only get called
// when the other Activity previously started using startActivityForResult
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
GoalKicker.com – Android™ Notes for Professionals 307
// First we need to check if the requestCode matches the one we used.
if(requestCode == REQUEST_CODE_EXAMPLE) {
// The resultCode is set by the DetailActivity
// By convention RESULT_OK means that whatever
// DetailActivity did was executed successfully
if(resultCode == Activity.RESULT_OK) {
// Get the result from the returned Intent
final String result = data.getStringExtra(DetailActivity.EXTRA_DATA);
// Use the data - in this case, display it in a Toast.
Toast.makeText(this, "Result: " + result, Toast.LENGTH_LONG).show();
} else {
// setResult wasn't successfully executed by DetailActivity
// Due to some error or flow of control. No data to retrieve.
}
}
}
}
DetailActivity:
public class DetailActivity extends Activity {
// Constant used to identify data sent between Activities.
public static final String EXTRA_DATA = "EXTRA_DATA";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
final Button button = (Button) findViewById(R.id.button);
// When this button is clicked we want to return a result
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Create a new Intent object as container for the result
final Intent data = new Intent();
// Add the required data to be returned to the MainActivity
data.putExtra(EXTRA_DATA, "Some interesting data!");
// Set the resultCode as Activity.RESULT_OK to
// indicate a success and attach the Intent
// which contains our result data
setResult(Activity.RESULT_OK, data);
// With finish() we close the DetailActivity to
// return back to MainActivity
finish();
}
});
}
@Override
public void onBackPressed() {
// When the user hits the back button set the resultCode
// as Activity.RESULT_CANCELED to indicate a failure
setResult(Activity.RESULT_CANCELED);
super.onBackPressed();
}
GoalKicker.com – Android™ Notes for Professionals 308
}
A few things you need to be aware of:
Data is only returned once you call finish(). You need to call setResult() before calling finish(),
otherwise, no result will be returned.
Make sure your Activity is not using android:launchMode="singleTask", or it will cause the Activity to
run in a separate task and therefore you will not receive a result from it. If your Activity uses singleTask as
launch mode, it will call onActivityResult() immediately with a result code of Activity.RESULT_CANCELED.
Be careful when using android:launchMode="singleInstance". On devices before Lollipop (Android 5.0, API
Level 21), Activities will not return a result.
You can use explicit or implicit intents when you call startActivityForResult(). When starting one of your
own activities to receive a result, you should use an explicit intent to ensure that you receive the expected
result. An explicit intent is always delivered to its target, no matter what it contains; the filter is not
consulted. But an implicit intent is delivered to a component only if it can pass through one of the
component's filters.
Section 41.2: Passing data between activities
This example illustrates sending a String with value as "Some data!" from OriginActivity to
DestinationActivity.
NOTE: This is the most straightforward way of sending data between two activities. See the example on using the
starter pattern for a more robust implementation.
OriginActivity
public class OriginActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_origin);
// Create a new Intent object, containing DestinationActivity as target Activity.
final Intent intent = new Intent(this, DestinationActivity.class);
// Add data in the form of key/value pairs to the intent object by using putExtra()
intent.putExtra(DestinationActivity.EXTRA_DATA, "Some data!");
// Start the target Activity with the intent object
startActivity(intent);
}
}
DestinationActivity
public class DestinationActivity extends AppCompatActivity {
public static final String EXTRA_DATA = "EXTRA_DATA";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_destination);
// getIntent() returns the Intent object which was used to start this Activity
final Intent intent = getIntent();
GoalKicker.com – Android™ Notes for Professionals 309
// Retrieve the data from the intent object by using the same key that
// was previously used to add data to the intent object in OriginActivity.
final String data = intent.getStringExtra(EXTRA_DATA);
}
}
It is also possible to pass other primitive data types as well as arrays, Bundle and Parcelable data. Passing
Serializable is also possible, but should be avoided as it is more than three times slower than Parcelable.
Serializable is a standard Java interface. You simply mark a class as Serializable by implementing the
Serializable interface and Java will automatically serialize it during required situations.
Parcelable is an Android specific interface which can be implemented on custom data types (i.e. your own objects
/ POJO objects ), it allows your object to be flattened and reconstruct itself without the destination needing to do
anything. There is a documentation example of making an object parcelable.
Once you have a parcelable object you can send it like a primitive type, with an intent object:
intent.putExtra(DestinationActivity.EXTRA_DATA, myParcelableObject);
Or in a bundle / as an argument for a fragment:
bundle.putParcelable(DestinationActivity.EXTRA_DATA, myParcelableObject);
and then also read it from the intent at the destination using getParcelableExtra:
final MyParcelableType data = intent.getParcelableExtra(EXTRA_DATA);
Or when reading in a fragment from a bundle:
final MyParcelableType data = bundle.getParcelable(EXTRA_DATA);
Once you have a Serializable object you can put it in an intent object:
bundle.putSerializable(DestinationActivity.EXTRA_DATA, mySerializableObject);
and then also read it from the intent object at the destination as shown below:
final SerializableType data = (SerializableType)bundle.getSerializable(EXTRA_DATA);
Section 41.3: Open a URL in a browser
Opening with the default browser
This example shows how you can open a URL programmatically in the built-in web browser rather than within your
application. This allows your app to open up a webpage without the need to include the INTERNET permission in
your manifest file.
public void onBrowseClick(View v) {
String url = "http://www.google.com";
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
// Verify that the intent will resolve to an activity
if (intent.resolveActivity(getPackageManager()) != null) {
// Here we use an intent without a Chooser unlike the next example
GoalKicker.com – Android™ Notes for Professionals 310
startActivity(intent);
}
}
Prompting the user to select a browser
Note that this example uses the Intent.createChooser() method:
public void onBrowseClick(View v) {
String url = "http://www.google.com";
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
// Note the Chooser below. If no applications match,
// Android displays a system message.So here there is no need for try-catch.
startActivity(Intent.createChooser(intent, "Browse with"));
}
In some cases, the URL may start with "www". If that is the case you will get this exception:
android.content.ActivityNotFoundException : No Activity found to handle Intent
The URL must always start with "http://" or "https://". Your code should therefore check for it, as shown in the
following code snippet:
if (!url.startsWith("https://") && !url.startsWith("http://")){
url = "http://" + url;
}
Intent openUrlIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
if (openUrlIntent.resolveActivity(getPackageManager()) != null) {
startActivity(openUrlIntent);
}
Best Practices
Check if there are no apps on the device that can receive the implicit intent. Otherwise, your app will crash when it
calls startActivity(). To first verify that an app exists to receive the intent, call resolveActivity() on your Intent
object. If the result is non-null, there is at least one app that can handle the intent and it's safe to call
startActivity(). If the result is null, you should not use the intent and, if possible, you should disable the feature
that invokes the intent.
Section 41.4: Starter Pattern
This pattern is a more strict approach to starting an Activity. Its purpose is to improve code readability, while at
the same time decrease code complexity, maintenance costs, and coupling of your components.
The following example implements the starter pattern, which is usually implemented as a static method on the
Activity itself. This static method accepts all required parameters, constructs a valid Intent from that data, and
then starts the Activity.
An Intent is an object that provides runtime binding between separate components, such as two activities. The
Intent represents an app’s "intent to do something." You can use intents for a wide variety of tasks, but here, your
intent starts another activity.
public class ExampleActivity extends AppCompatActivity {
GoalKicker.com – Android™ Notes for Professionals 311
private static final String EXTRA_DATA = "EXTRA_DATA";
public static void start(Context context, String data) {
Intent intent = new Intent(context, ExampleActivity.class);
intent.putExtra(EXTRA_DATA, data);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if(!intent.getExtras().containsKey(EXTRA_DATA)){
throw new UnsupportedOperationException("Activity should be started using the static
start method");
}
String data = intent.getStringExtra(EXTRA_DATA);
}
}
This pattern also allows you to force additional data to be passed with the intent.
The ExampleActivity can then be started like this, where context is an activity context:
ExampleActivity.start(context, "Some data!");
Section 41.5: Clearing an activity stack
Sometimes you may want to start a new activity while removing previous activities from the back stack, so the back
button doesn't take you back to them. One example of this might be starting an app on the Login activity, taking
you through to the Main activity of your application, but on logging out you want to be directed back to Login
without a chance to go back. In a case like that you can set the FLAG_ACTIVITY_CLEAR_TOP flag for the intent,
meaning if the activity being launched is already running in the current task (LoginActivity), then instead of
launching a new instance of that activity, all of the other activities on top of it will be closed and this Intent will be
delivered to the (now on top) old activity as a new Intent.
Intent intent = new Intent(getApplicationContext(), LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
It's also possible to use the flags FLAG_ACTIVITY_NEW_TASK along with FLAG_ACTIVITY_CLEAR_TASK if you want to
clear all Activities on the back stack:
Intent intent = new Intent(getApplicationContext(), LoginActivity.class);
// Closing all the Activities, clear the back stack.
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
Section 41.6: Start an activity
This example will start DestinationActivity from OriginActivity.
Here, the Intent constructor takes two parameters:
1. A Context as its first parameter (this is used because the Activity class is a subclass of Context)
GoalKicker.com – Android™ Notes for Professionals 312
2. The Class of the app component to which the system should deliver the Intent (in this case, the activity that
should be started)
public class OriginActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_origin);
Intent intent = new Intent(this, DestinationActivity.class);
startActivity(intent);
finish(); // Optionally, you can close OriginActivity. In this way when the user press back
from DestinationActivity he/she won't land on OriginActivity again.
}
}
Another way to create the Intent to open DestinationActivity is to use the default constructor for the Intent,
and use the setClass() method to tell it which Activity to open:
Intent i=new Intent();
i.setClass(this, DestinationActivity.class);
startActivity(intent);
finish(); // Optionally, you can close OriginActivity. In this way when the user press back from
DestinationActivity he/she won't land on OriginActivity
Section 41.7: Sending emails
// Compile a Uri with the 'mailto' schema
Intent emailIntent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts(
"mailto","johndoe@example.com", null));
// Subject
emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Hello World!");
// Body of email
emailIntent.putExtra(Intent.EXTRA_TEXT, "Hi! I am sending you a test email.");
// File attachment
emailIntent.putExtra(Intent.EXTRA_STREAM, attachedFileUri);
// Check if the device has an email client
if (emailIntent.resolveActivity(getPackageManager()) != null) {
// Prompt the user to select a mail app
startActivity(Intent.createChooser(emailIntent,"Choose your mail application"));
} else {
// Inform the user that no email clients are installed or provide an alternative
}
This will pre-fill an email in a mail app of the user's choice.
If you need to add an attachment, you can use Intent.ACTION_SEND instead of Intent.ACTION_SENDTO. For multiple
attachments you can use ACTION_SEND_MULTIPLE
A word of caution: not every device has a provider for ACTION_SENDTO, and calling startActivity() without
checking with resolveActivity() first may throw an ActivityNotFoundException.
Section 41.8: CustomTabsIntent for Chrome Custom Tabs
Version ≥ 4.0.3
GoalKicker.com – Android™ Notes for Professionals 313
Using a CustomTabsIntent, it is now possible to configure Chrome custom tabs in order to customize key UI
components in the browser that is opened from your app.
This is a good alternative to using a WebView for some cases. It allows loading of a web page with an Intent, with
the added ability to inject some degree of the look and feel of your app into the browser.
Here is an example of how to open a url using CustomTabsIntent
String url = "https://www.google.pl/";
CustomTabsIntent intent = new CustomTabsIntent.Builder()
.setStartAnimations(getContext(), R.anim.slide_in_right, R.anim.slide_out_left)
.setExitAnimations(getContext(), android.R.anim.slide_in_left,
android.R.anim.slide_out_right)
.setCloseButtonIcon(BitmapFactory.decodeResource(getResources(),
R.drawable.ic_arrow_back_white_24dp))
.setToolbarColor(Color.parseColor("#43A047"))
.enableUrlBarHiding()
.build();
intent.launchUrl(getActivity(), Uri.parse(url));
Note:
To use custom tabs, you need to add this dependency to your build.gradle
compile 'com.android.support:customtabs:24.1.1'
Section 41.9: Intent URI
This example shows, how to start intent from browser:
<a href="intent://host.com/path#Intent;package=com.sample.test;scheme=yourscheme;end">Start
intent</a>
This intent will start app with package com.sample.test or will open google play with this package.
Also this intent can be started with javascript:
var intent = "intent://host.com/path#Intent;package=com.sample.test;scheme=yourscheme;end";
window.location.replace(intent)
In activity this host and path can be obtained from intent data:
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
Uri data = getIntent().getData(); // returns host.com/path
}
Intent URI syntax:
HOST/URI-path // Optional host
#Intent;
package=[string];
action=[string];
category=[string];
component=[string];
scheme=[string];
GoalKicker.com – Android™ Notes for Professionals 314
end;
Section 41.10: Start the dialer
This example shows how to open a default dialer (an app that makes regular calls) with a provided telephone
number already in place:
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:9988776655")); //Replace with valid phone number. Remember to add the
tel: prefix, otherwise it will crash.
startActivity(intent);
Result from running the code above:
Section 41.11: Broadcasting Messages to Other Components
Intents can be used to broadcast messages to other components of your application (such as a running background
service) or to the entire Android system.
To send a broadcast within your application, use the LocalBroadcastManager class:
Intent intent = new Intent("com.example.YOUR_ACTION"); // the intent action
intent.putExtra("key", "value"); // data to be passed with your broadcast
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context);
manager.sendBroadcast(intent);
GoalKicker.com – Android™ Notes for Professionals 315
To send a broadcast to components outside of your application, use the sendBroadcast() method on a Context
object.
Intent intent = new Intent("com.example.YOUR_ACTION"); // the intent action
intent.putExtra("key", "value"); // data to be passed with your broadcast
context.sendBroadcast(intent);
Information about receiving broadcasts can be found here: Broadcast Receiver
Section 41.12: Passing custom object between activities
It is also possible to pass your custom object to other activities using the Bundle class.
There are two ways:
Serializable interface—for Java and Android
Parcelable interface—memory efficient, only for Android (recommended)
Parcelable
Parcelable processing is much faster than serializable. One of the reasons for this is that we are being explicit about
the serialization process instead of using reflection to infer it. It also stands to reason that the code has been
heavily optimized for this purpose.
public class MyObjects implements Parcelable {
private int age;
private String name;
private ArrayList<String> address;
public MyObjects(String name, int age, ArrayList<String> address) {
this.name = name;
this.age = age;
this.address = address;
}
public MyObjects(Parcel source) {
age = source.readInt();
name = source.readString();
address = source.createStringArrayList();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(age);
dest.writeString(name);
dest.writeStringList(address);
}
public int getAge() {
return age;
GoalKicker.com – Android™ Notes for Professionals 316
}
public String getName() {
return name;
}
public ArrayList<String> getAddress() {
if (!(address == null))
return address;
else
return new ArrayList<String>();
}
public static final Creator<MyObjects> CREATOR = new Creator<MyObjects>() {
@Override
public MyObjects[] newArray(int size) {
return new MyObjects[size];
}
@Override
public MyObjects createFromParcel(Parcel source) {
return new MyObjects(source);
}
};
}
Sending Activity Code
MyObject mObject = new MyObject("name","age","Address array here");
//Passing MyOject
Intent mIntent = new Intent(FromActivity.this, ToActivity.class);
mIntent.putExtra("UniqueKey", mObject);
startActivity(mIntent);
Receiving the object in destination activity.
//Getting MyObjects
Intent mIntent = getIntent();
MyObjects workorder = (MyObjects) mIntent.getParcelable("UniqueKey");
You can pass Arraylist of Parceble object as below
//Array of MyObjects
ArrayList<MyObject> mUsers;
//Passing MyObject List
Intent mIntent = new Intent(FromActivity.this, ToActivity.class);
mIntent.putParcelableArrayListExtra("UniqueKey", mUsers);
startActivity(mIntent);
//Getting MyObject List
Intent mIntent = getIntent();
ArrayList<MyObjects> mUsers = mIntent.getParcelableArrayList("UniqueKey");
Note: There are Android Studio plugins such as this one available to generate Parcelable code
Serializable
GoalKicker.com – Android™ Notes for Professionals 317
Sending Activity Code
Product product = new Product();
Bundle bundle = new Bundle();
bundle.putSerializable("product", product);
Intent cartIntent = new Intent(mContext, ShowCartActivity.class);
cartIntent.putExtras(bundle);
mContext.startActivity(cartIntent);
Receiving the object in destination activity.
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle bundle = this.getIntent().getExtras();
Product product = null;
if (bundle != null) {
product = (Product) bundle.getSerializable("product");
}
Arraylist of Serializable object: same as single object passing
Custom object should implement the Serializable interface.
Section 41.13: Open Google map with specified latitude,
longitude
You can pass latitude, longitude from your app to Google map using Intent
String uri = String.format(Locale.ENGLISH, "http://maps.google.com/maps?q=loc:%f,%f",
28.43242324,77.8977673);
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
startActivity(intent);
Section 41.14: Passing dierent data through Intent in Activity
1. Passing integer data:
SenderActivity
Intent myIntent = new Intent(SenderActivity.this, ReceiverActivity.class);
myIntent.putExtra("intVariableName", intValue);
startActivity(myIntent);
ReceiverActivity
Intent mIntent = getIntent();
int intValue = mIntent.getIntExtra("intVariableName", 0); // set 0 as the default value if no value
for intVariableName found
2. Passing double data:
SenderActivity
Intent myIntent = new Intent(SenderActivity.this, ReceiverActivity.class);
myIntent.putExtra("doubleVariableName", doubleValue);
startActivity(myIntent);
GoalKicker.com – Android™ Notes for Professionals 318
ReceiverActivity
Intent mIntent = getIntent();
double doubleValue = mIntent.getDoubleExtra("doubleVariableName", 0.00); // set 0.00 as the default
value if no value for doubleVariableName found
3. Passing String data:
SenderActivity
Intent myIntent = new Intent(SenderActivity.this, ReceiverActivity.class);
myIntent.putExtra("stringVariableName", stringValue);
startActivity(myIntent);
ReceiverActivity
Intent mIntent = getIntent();
String stringValue = mIntent.getExtras().getString("stringVariableName");
or
Intent mIntent = getIntent();
String stringValue = mIntent.getStringExtra("stringVariableName");
4. Passing ArrayList data :
SenderActivity
Intent myIntent = new Intent(SenderActivity.this, ReceiverActivity.class);
myIntent.putStringArrayListExtra("arrayListVariableName", arrayList);
startActivity(myIntent);
ReceiverActivity
Intent mIntent = getIntent();
arrayList = mIntent.getStringArrayListExtra("arrayListVariableName");
5. Passing Object data :
SenderActivity
Intent myIntent = new Intent(SenderActivity.this, ReceiverActivity.class);
myIntent.putExtra("ObjectVariableName", yourObject);
startActivity(myIntent);
ReceiverActivity
Intent mIntent = getIntent();
yourObj = mIntent.getSerializableExtra("ObjectVariableName");
Note : Keep in mind your custom Class must implement the Serializable interface.
6. Passing HashMap<String, String> data :
GoalKicker.com – Android™ Notes for Professionals 319
SenderActivity
HashMap<String, String> hashMap;
Intent mIntent = new Intent(SenderActivity.this, ReceiverActivity.class);
mIntent.putExtra("hashMap", hashMap);
startActivity(mIntent);
ReceiverActivity
Intent mIntent = getIntent();
HashMap<String, String> hashMap = (HashMap<String, String>)
mIntent.getSerializableExtra("hashMap");
7. Passing Bitmap data :
SenderActivity
Intent myIntent = new Intent(SenderActivity.this, ReceiverActivity.class);
myIntent.putExtra("image",bitmap);
startActivity(mIntent);
ReceiverActivity
Intent mIntent = getIntent();
Bitmap bitmap = mIntent.getParcelableExtra("image");
Section 41.15: Share intent
Share simple information with differents apps.
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, "This is my text to send.");
sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_to)));
Share an image with differents apps.
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_STREAM, uriToImage);
shareIntent.setType("image/jpeg");
startActivity(Intent.createChooser(shareIntent, getResources().getText(R.string.send_to)));
Section 41.16: Showing a File Chooser and Reading the Result
Starting a File Chooser Activity
public void showFileChooser() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
// Update with mime types
intent.setType("*/*");
// Update with additional mime types here using a String[].
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
GoalKicker.com – Android™ Notes for Professionals 320
// Only pick openable and local files. Theoretically we could pull files from google drive
// or other applications that have networked files, but that's unnecessary for this example.
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
// REQUEST_CODE = <some-integer>
startActivityForResult(intent, REQUEST_CODE);
}
Reading the Result
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// If the user doesn't pick a file just return
if (requestCode != REQUEST_CODE || resultCode != RESULT_OK) {
return;
}
// Import the file
importFile(data.getData());
}
public void importFile(Uri uri) {
String fileName = getFileName(uri);
// The temp file could be whatever you want
File fileCopy = copyToTempFile(uri, File tempFile)
// Done!
}
/**
* Obtains the file name for a URI using content resolvers. Taken from the following link
* https://developer.android.com/training/secure-file-sharing/retrieve-info.html#RetrieveFileInfo
*
* @param uri a uri to query
* @return the file name with no path
* @throws IllegalArgumentException if the query is null, empty, or the column doesn't exist
*/
private String getFileName(Uri uri) throws IllegalArgumentException {
// Obtain a cursor with information regarding this uri
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
if (cursor.getCount() <= 0) {
cursor.close();
throw new IllegalArgumentException("Can't obtain file name, cursor is empty");
}
cursor.moveToFirst();
String fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
cursor.close();
return fileName;
}
/**
* Copies a uri reference to a temporary file
*
* @param uri the uri used as the input stream
* @param tempFile the file used as an output stream
GoalKicker.com – Android™ Notes for Professionals 321
* @return the input tempFile for convenience
* @throws IOException if an error occurs
*/
private File copyToTempFile(Uri uri, File tempFile) throws IOException {
// Obtain an input stream from the uri
InputStream inputStream = getContentResolver().openInputStream(uri);
if (inputStream == null) {
throw new IOException("Unable to obtain input stream from URI");
}
// Copy the stream to the temp file
FileUtils.copyInputStreamToFile(inputStream, tempFile);
return tempFile;
}
Section 41.17: Sharing Multiple Files through Intent
The String List passed as a parameter to the share() method contains the paths of all the files you want to share.
It basically loops through the paths, adds them to Uri, and starts the Activity which can accept Files of this type.
public static void share(AppCompatActivity context,List<String> paths) {
if (paths == null || paths.size() == 0) {
return;
}
ArrayList<Uri> uris = new ArrayList<>();
Intent intent = new Intent();
intent.setAction(android.content.Intent.ACTION_SEND_MULTIPLE);
intent.setType("*/*");
for (String path : paths) {
File file = new File(path);
uris.add(Uri.fromFile(file));
}
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
context.startActivity(intent);
}
Section 41.18: Start Unbound Service using an Intent
A Service is a component which runs in the background (on the UI thread) without direct interaction with the user.
An unbound Service is just started, and is not bound to the lifecycle of any Activity.
To start a Service you can do as shown in the example below:
// This Intent will be used to start the service
Intent i= new Intent(context, ServiceName.class);
// potentially add data to the intent extras
i.putExtra("KEY1", "Value to be used by the service");
context.startService(i);
You can use any extras from the intent by using an onStartCommand() override:
public class MyService extends Service {
public MyService() {
}
GoalKicker.com – Android™ Notes for Professionals 322
@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
if (intent != null) {
Bundle extras = intent.getExtras();
String key1 = extras.getString("KEY1", "");
if (key1.equals("Value to be used by the service")) {
//do something
}
}
return START_STICKY;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
Section 41.19: Getting a result from Activity to Fragment
Like Getting a result from another Activity you need to call the Fragment's method
startActivityForResult(Intent intent, int requestCode). note that you should not call
getActivity().startActivityForResult() as this will take the result back to the Fragment's parent Activity.
Receiving the result can be done using the Fragment's method onActivityResult(). You need to make sure that
the Fragment's parent Activity also overrides onActivityResult() and calls it's super implementation.
In the following example ActivityOne contains FragmentOne, which will start ActivityTwo and expect a result from
it.
ActivityOne
public class ActivityOne extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_one);
}
// You must override this method as the second Activity will always send its results to this
Activity and then to the Fragment
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
}
}
activity_one.xml
<fragment android:name="com.example.FragmentOne"
android:id="@+id/fragment_one"
android:layout_width="match_parent"
android:layout_height="match_parent" />
FragmentOne
GoalKicker.com – Android™ Notes for Professionals 323
public class FragmentOne extends Fragment {
public static final int REQUEST_CODE = 11;
public static final int RESULT_CODE = 12;
public static final String EXTRA_KEY_TEST = "testKey";
// Initializing and starting the second Activity
private void startSecondActivity() {
Intent intent = new Intent(getActivity(), ActivityTwo.class);
startActivityForResult(REQUEST_CODE, intent);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE && resultCode == RESULT_CODE) {
String testResult = data.getStringExtra(EXTRA_KEY_TEST);
// TODO: Do something with your extra data
}
}
}
ActivityTwo
public class ActivityTwo extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_two);
}
private void closeActivity() {
Intent intent = new Intent();
intent.putExtra(FragmentOne.EXTRA_KEY_TEST, "Testing passing data back to ActivityOne");
setResult(FragmentOne.RESULT_CODE, intent); // You can also send result without any data
using setResult(int resultCode)
finish();
}
}
GoalKicker.com – Android™ Notes for Professionals 324
Chapter 42: Fragments
Introduction about Fragments and their intercommunication mechanism
Section 42.1: Pass data from Activity to Fragment using
Bundle
All fragments should have an empty constructor (i.e. a constructor method having no input arguments). Therefore,
in order to pass your data to the Fragment being created, you should use the setArguments() method. This
methods gets a bundle, which you store your data in, and stores the Bundle in the arguments. Subsequently, this
Bundle can then be retrieved in onCreate() and onCreateView() call backs of the Fragment.
Activity:
Bundle bundle = new Bundle();
String myMessage = "Stack Overflow is cool!";
bundle.putString("message", myMessage );
FragmentClass fragInfo = new FragmentClass();
fragInfo.setArguments(bundle);
transaction.replace(R.id.fragment_single, fragInfo);
transaction.commit();
Fragment:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
String myValue = this.getArguments().getString("message");
...
}
Section 42.2: The newInstance() pattern
Although it is possible to create a fragment constructor with parameters, Android internally calls the zero-argument
constructor when recreating fragments (for example, if they are being restored after being killed for Android's own
reasons). For this reason, it is not advisable to rely on a constructor that has parameters.
To ensure that your expected fragment arguments are always present you can use a static newInstance() method
to create the fragment, and put whatever parameters you want in to a bundle that will be available when creating a
new instance.
import android.os.Bundle;
import android.support.v4.app.Fragment;
public class MyFragment extends Fragment
{
// Our identifier for obtaining the name from arguments
private static final String NAME_ARG = "name";
private String mName;
// Required
public MyFragment(){}
// The static constructor. This is the only way that you should instantiate
// the fragment yourself
GoalKicker.com – Android™ Notes for Professionals 325
public static MyFragment newInstance(final String name) {
final MyFragment myFragment = new MyFragment();
// The 1 below is an optimization, being the number of arguments that will
// be added to this bundle. If you know the number of arguments you will add
// to the bundle it stops additional allocations of the backing map. If
// unsure, you can construct Bundle without any arguments
final Bundle args = new Bundle(1);
// This stores the argument as an argument in the bundle. Note that even if
// the 'name' parameter is NULL then this will work, so you should consider
// at this point if the parameter is mandatory and if so check for NULL and
// throw an appropriate error if so
args.putString(NAME_ARG, name);
myFragment.setArguments(args);
return myFragment;
}
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Bundle arguments = getArguments();
if (arguments == null || !arguments.containsKey(NAME_ARG)) {
// Set a default or error as you see fit
} else {
mName = arguments.getString(NAME_ARG);
}
}
}
Now, in the Activity:
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
MyFragment mFragment = MyFragment.newInstance("my name");
ft.replace(R.id.placeholder, mFragment);
//R.id.placeholder is where we want to load our fragment
ft.commit();
This pattern is a best practice to ensure that all the needed arguments will be passed to fragments on creation.
Note that when the system destroys the fragment and re-creates it later, it will automatically restore its state - but
you must provide it with an onSaveInstanceState(Bundle) implementation.
Section 42.3: Navigation between fragments using backstack
and static fabric pattern
First of all, we need to add our first Fragment at the beginning, we should do it in the onCreate() method of our
Activity:
if (null == savedInstanceState) {
getSupportFragmentManager().beginTransaction()
.addToBackStack("fragmentA")
.replace(R.id.container, FragmentA.newInstance(), "fragmentA")
.commit();
}
Next, we need to manage our backstack. The easiest way is using a function added in our activity that is used for all
FragmentTransactions.
GoalKicker.com – Android™ Notes for Professionals 326
public void replaceFragment(Fragment fragment, String tag) {
//Get current fragment placed in container
Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.container);
//Prevent adding same fragment on top
if (currentFragment.getClass() == fragment.getClass()) {
return;
}
//If fragment is already on stack, we can pop back stack to prevent stack infinite growth
if (getSupportFragmentManager().findFragmentByTag(tag) != null) {
getSupportFragmentManager().popBackStack(tag, FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
//Otherwise, just replace fragment
getSupportFragmentManager()
.beginTransaction()
.addToBackStack(tag)
.replace(R.id.container, fragment, tag)
.commit();
}
Finally, we should override onBackPressed() to exit the application when going back from the last Fragment
available in the backstack.
@Override
public void onBackPressed() {
int fragmentsInStack = getSupportFragmentManager().getBackStackEntryCount();
if (fragmentsInStack > 1) { // If we have more than one fragment, pop back stack
getSupportFragmentManager().popBackStack();
} else if (fragmentsInStack == 1) { // Finish activity, if only one fragment left, to prevent
leaving empty screen
finish();
} else {
super.onBackPressed();
}
}
Execution in activity:
replaceFragment(FragmentB.newInstance(), "fragmentB");
Execution outside activity (assuming MainActivity is our activity):
((MainActivity) getActivity()).replaceFragment(FragmentB.newInstance(), "fragmentB");
Section 42.4: Sending events back to an activity with callback
interface
If you need to send events from fragment to activity, one of the possible solutions is to define callback interface and
require that the host activity implement it.
Example
Send callback to an activity, when fragment's button clicked
First of all, define callback interface:
public interface SampleCallback {
void onButtonClicked();
GoalKicker.com – Android™ Notes for Professionals 327
}
Next step is to assign this callback in fragment:
public final class SampleFragment extends Fragment {
private SampleCallback callback;
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof SampleCallback) {
callback = (SampleCallback) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement SampleCallback");
}
}
@Override
public void onDetach() {
super.onDetach();
callback = null;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
savedInstanceState) {
final View view = inflater.inflate(R.layout.sample, container, false);
// Add button's click listener
view.findViewById(R.id.actionButton).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
callback.onButtonClicked(); // Invoke callback here
}
});
return view;
}
}
And finally, implement callback in activity:
public final class SampleActivity extends Activity implements SampleCallback {
// ... Skipped code with settings content view and presenting the fragment
@Override
public void onButtonClicked() {
// Invoked when fragment's button has been clicked
}
}
Section 42.5: Animate the transition between fragments
To animate the transition between fragments, or to animate the process of showing or hiding a fragment you use
the FragmentManager to create a FragmentTransaction.
For a single FragmentTransaction, there are two different ways to perform animations: you can use a standard
animation or you can supply your own custom animations.
GoalKicker.com – Android™ Notes for Professionals 328
Standard animations are specified by calling FragmentTransaction.setTransition(int transit), and using one
of the pre-defined constants available in the FragmentTransaction class. At the time of writing, these constants are:
FragmentTransaction.TRANSIT_NONE
FragmentTransaction.TRANSIT_FRAGMENT_OPEN
FragmentTransaction.TRANSIT_FRAGMENT_CLOSE
FragmentTransaction.TRANSIT_FRAGMENT_FADE
The complete transaction might look something like this:
getSupportFragmentManager()
.beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.replace(R.id.contents, new MyFragment(), "MyFragmentTag")
.commit();
Custom animations are specified by calling either FragmentTransaction.setCustomAnimations(int enter, int
exit) or FragmentTransaction.setCustomAnimations(int enter, int exit, int popEnter, int popExit).
The enter and exit animations will be played for FragmentTransactions that do not involve popping fragments off
of the back stack. The popEnter and popExit animations will be played when popping a fragment off of the back
stack.
The following code shows how you would replace a fragment by sliding out one fragment and sliding the other one
in it's place.
getSupportFragmentManager()
.beginTransaction()
.setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right)
.replace(R.id.contents, new MyFragment(), "MyFragmentTag")
.commit();
The XML animation definitions would use the objectAnimator tag. An example of slide_in_left.xml might look
something like this:
<?xml version="1.0" encoding="utf-8"?>
<set>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="x"
android:valueType="floatType"
android:valueFrom="-1280"
android:valueTo="0"
android:duration="500"/>
</set>
Section 42.6: Communication between Fragments
All communications between Fragments must go via an Activity. Fragments CANNOT communicate with each other
without an Activity.
Additional Resources
How to implement OnFragmentInteractionListener
Android | Communicating With Other Fragments
In this sample, we have a MainActivity that hosts two fragments, SenderFragment and ReceiverFragment, for
GoalKicker.com – Android™ Notes for Professionals 329
sending and receiving a message (a simple String in this case) respectively.
A Button in SenderFragment initiates the process of sending the message. A TextView in the ReceiverFragment is
updated when the message is received by it.
Following is the snippet for the MainActivity with comments explaining the important lines of code:
// Our MainActivity implements the interface defined by the SenderFragment. This enables
// communication from the fragment to the activity
public class MainActivity extends AppCompatActivity implements SenderFragment.SendMessageListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
/**
* This method is called when we click on the button in the SenderFragment
* @param message The message sent by the SenderFragment
*/
@Override
public void onSendMessage(String message) {
// Find our ReceiverFragment using the SupportFragmentManager and the fragment's id
ReceiverFragment receiverFragment = (ReceiverFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_receiver);
// Make sure that such a fragment exists
if (receiverFragment != null) {
// Send this message to the ReceiverFragment by calling its public method
receiverFragment.showMessage(message);
}
}
}
The layout file for the MainActivity hosts two fragments inside a LinearLayout :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.naru.fragmentcommunication.MainActivity">
<fragment
android:id="@+id/fragment_sender"
android:name="com.naru.fragmentcommunication.SenderFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
tools:layout="@layout/fragment_sender" />
<fragment
android:id="@+id/fragment_receiver"
android:name="com.naru.fragmentcommunication.ReceiverFragment"
GoalKicker.com – Android™ Notes for Professionals 330
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
tools:layout="@layout/fragment_receiver" />
</LinearLayout>
The SenderFragment exposes an interface SendMessageListener that helps the MainActivity know when Button in
the SenderFragment was clicked.
Following is the code snippet for the SenderFragment explaining the important lines of code:
public class SenderFragment extends Fragment {
private SendMessageListener commander;
/**
* This interface is created to communicate between the activity and the fragment. Any activity
* which implements this interface will be able to receive the message that is sent by this
* fragment.
*/
public interface SendMessageListener {
void onSendMessage(String message);
}
/**
* API LEVEL >= 23
* <p>
* This method is called when the fragment is attached to the activity. This method here will
* help us to initialize our reference variable, 'commander' , for our interface
* 'SendMessageListener'
*
* @param context
*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
// Try to cast the context to our interface SendMessageListener i.e. check whether the
// activity implements the SendMessageListener. If not a ClassCastException is thrown.
try {
commander = (SendMessageListener) context;
} catch (ClassCastException e) {
throw new ClassCastException(context.toString()
+ "must implement the SendMessageListener interface");
}
}
/**
* API LEVEL < 23
* <p>
* This method is called when the fragment is attached to the activity. This method here will
* help us to initialize our reference variable, 'commander' , for our interface
* 'SendMessageListener'
*
* @param activity
*/
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// Try to cast the context to our interface SendMessageListener i.e. check whether the
// activity implements the SendMessageListener. If not a ClassCastException is thrown.
try {
GoalKicker.com – Android™ Notes for Professionals 331
commander = (SendMessageListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ "must implement the SendMessageListener interface");
}
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
// Inflate view for the sender fragment.
View view = inflater.inflate(R.layout.fragment_receiver, container, false);
// Initialize button and a click listener on it
Button send = (Button) view.findViewById(R.id.bSend);
send.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Sanity check whether we were able to properly initialize our interface reference
if (commander != null) {
// Call our interface method. This enables us to call the implemented method
// in the activity, from where we can send the message to the ReceiverFragment.
commander.onSendMessage("HELLO FROM SENDER FRAGMENT!");
}
}
});
return view;
}
}
The layout file for the SenderFragment:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/bSend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SEND"
android:layout_gravity="center_horizontal" />
</LinearLayout>
The ReceiverFragment is simple and exposes a simple public method to updates its TextView. When the
MainActivity receives the message from the SenderFragment it calls this public method of the ReceiverFragment
Following is the code snippet for the ReceiverFragment with comments explaining the important lines of code :
public class ReceiverFragment extends Fragment {
TextView tvMessage;
@Nullable
GoalKicker.com – Android™ Notes for Professionals 332
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
// Inflate view for the sender fragment.
View view = inflater.inflate(R.layout.fragment_receiver, container, false);
// Initialize the TextView
tvMessage = (TextView) view.findViewById(R.id.tvReceivedMessage);
return view;
}
/**
* Method that is called by the MainActivity when it receives a message from the SenderFragment.
* This method helps update the text in the TextView to the message sent by the SenderFragment.
* @param message Message sent by the SenderFragment via the MainActivity.
*/
public void showMessage(String message) {
tvMessage.setText(message);
}
}
The layout file for the ReceiverFragment :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tvReceivedMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Waiting for message!" />
</LinearLayout>
GoalKicker.com – Android™ Notes for Professionals 333
Chapter 43: Button
Section 43.1: Using the same click event for one or more
Views in the XML
When we create any View in layout, we can use the android:onClick attribute to reference a method in the
associated activity or fragment to handle the click events.
XML Layout
<Button android:id="@+id/button"
...
// onClick should reference the method in your activity or fragment
android:onClick="doSomething" />
// Note that this works with any class which is a subclass of View, not just Button
<ImageView android:id="@+id/image"
...
android:onClick="doSomething" />
Activity/fragment code
In your code, create the method you named, where v will be the view that was touched, and do something for each
view that calls this method.
public void doSomething(View v) {
switch(v.getId()) {
case R.id.button:
// Button was clicked, do something.
break;
case R.id.image:
// Image was clicked, do something else.
break;
}
}
If you want, you can also use different method for each View (in this case, of course, you don't have to check for the
ID).
Section 43.2: Defining external Listener
When should I use it
When the code inside an inline listener is too big and your method / class becomes ugly and hard to read
You want to perform same action in various elements (view) of your app
To achieve this you need to create a class implementing one of the listeners in the View API.
For example, give help when long click on any element:
public class HelpLongClickListener implements View.OnLongClickListener
{
public HelpLongClickListener() {
}
@Override
GoalKicker.com – Android™ Notes for Professionals 334
public void onLongClick(View v) {
// show help toast or popup
}
}
Then you just need to have an attribute or variable in your Activity to use it:
HelpLongClickListener helpListener = new HelpLongClickListener(...);
button1.setOnClickListener(helpListener);
button2.setOnClickListener(helpListener);
label.setOnClickListener(helpListener);
button1.setOnClickListener(helpListener);
NOTE: defining listeners in separated class has one disadvantage, it cannot access class fields directly, so you need
to pass data (context, view) through constructor unless you make attributes public or define geters.
Section 43.3: inline onClickListener
Say we have a button (we can create it programmatically, or bind it from a view using findViewbyId(), etc...)
Button btnOK = (...)
Now, create an anonymous class and set it inline.
btnOk.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Do stuff here...
}
});
Section 43.4: Customizing Button style
There are many possible ways of customizing the look of a Button. This example presents several options:
Option 0: Use ThemeOverlay (currently the easiest/quickest way)
Create a new style in your styles file:
styles.xml
<resources>
<style name=“mybutton” parent=”ThemeOverlay.AppCompat.Ligth”>
<!-- customize colorButtonNormal for the disable color -->
<item name="colorButtonNormal">@color/colorbuttonnormal</item>
<!-- customize colorAccent for the enabled color -->
<item name="colorButtonNormal">@color/coloraccent</item>
</style>
</resources>
Then in the layout where you place your button (e.g. MainActivity):
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
GoalKicker.com – Android™ Notes for Professionals 335
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
<Button
android:id="@+id/mybutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello"
android:theme="@style/mybutton"
style="@style/Widget.AppCompat.Button.Colored"/>
</LinearLayout>
Option 1: Create your own button style
In values/styles.xml, create a new style for your button:
styles.xml
<resources>
<style name="mybuttonstyle" parent="@android:style/Widget.Button">
<item name="android:gravity">center_vertical|center_horizontal</item>
<item name="android:textColor">#FFFFFFFF</item>
<item name="android:shadowColor">#FF000000</item>
<item name="android:shadowDx">0</item>
<item name="android:shadowDy">-1</item>
<item name="android:shadowRadius">0.2</item>
<item name="android:textSize">16dip</item>
<item name="android:textStyle">bold</item>
<item name="android:background">@drawable/button</item>
</style>
</resources>
Then in the layout where you place your button (e.g. in MainActivity):
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
GoalKicker.com – Android™ Notes for Professionals 336
<Button
android:id="@+id/mybutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello"
android:theme="@style/mybuttonstyle"/>
</LinearLayout>
Option 2: Assign a drawable for each of your button states
Create an xml file into drawable folder called 'mybuttondrawable.xml' to define the drawable resource of each of
your button states:
drawable/mybutton.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_enabled="false"
android:drawable="@drawable/mybutton_disabled" />
<item
android:state_pressed="true"
android:state_enabled="true"
android:drawable="@drawable/mybutton_pressed" />
<item
android:state_focused="true"
android:state_enabled="true"
android:drawable="@drawable/mybutton_focused" />
<item
android:state_enabled="true"
android:drawable="@drawable/mybutton_enabled" />
</selector>
Each of those drawables may be images (e.g. mybutton_disabled.png) or xml files defined by you and stored in the
drawables folder. For instance:
drawable/mybutton_disabled.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<gradient
android:startColor="#F2F2F2"
android:centerColor="#A4A4A4"
android:endColor="#F2F2F2"
android:angle="90"/>
<padding android:left="7dp"
android:top="7dp"
android:right="7dp"
android:bottom="7dp" />
<stroke
android:width="2dip"
android:color="#FFFFFF" />
<corners android:radius= "8dp" />
</shape>
Then in the layout where you place your button (e.g. MainActivity):
activity_main.xml
GoalKicker.com – Android™ Notes for Professionals 337
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
<Button
android:id="@+id/mybutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello"
android:background="@drawable/mybuttondrawable"/>
</LinearLayout>
Option 3: Add your button style to your App theme
You can override the default android button style in the definition of your app theme (in values/styles.xml).
styles.xml
<resources>
<style name="AppTheme" parent="android:Theme">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:button">@style/mybutton</item>
</style>
<style name="mybutton" parent="android:style/Widget.Button">
<item name="android:gravity">center_vertical|center_horizontal</item>
<item name="android:textColor">#FFFFFFFF</item>
<item name="android:shadowColor">#FF000000</item>
<item name="android:shadowDx">0</item>
<item name="android:shadowDy">-1</item>
<item name="android:shadowRadius">0.2</item>
<item name="android:textSize">16dip</item>
<item name="android:textStyle">bold</item>
<item name="android:background">@drawable/anydrawable</item>
</style>
</resources>
Option 4: Overlay a color on the default button style programmatically
Just find you button in your activity and apply a color filter:
Button mybutton = (Button) findViewById(R.id.mybutton);
mybutton.getBackground().setColorFilter(anycolor, PorterDuff.Mode.MULTIPLY)
You can check different blending modes here and nice examples here.
GoalKicker.com – Android™ Notes for Professionals 338
Section 43.5: Custom Click Listener to prevent multiple fast
clicks
In order to prevent a button from firing multiple times within a short period of time (let's say 2 clicks within 1
second, which may cause serious problems if the flow is not controlled), one can implement a custom
SingleClickListener.
This ClickListener sets a specific time interval as threshold (for instance, 1000ms).
When the button is clicked, a check will be ran to see if the trigger was executed in the past amount of time you
defined, and if not it will trigger it.
public class SingleClickListener implements View.OnClickListener {
protected int defaultInterval;
private long lastTimeClicked = 0;
public SingleClickListener() {
this(1000);
}
public SingleClickListener(int minInterval) {
this.defaultInterval = minInterval;
}
@Override
public void onClick(View v) {
if (SystemClock.elapsedRealtime() - lastTimeClicked < defaultInterval) {
return;
}
lastTimeClicked = SystemClock.elapsedRealtime();
performClick(v);
}
public abstract void performClick(View v);
}
And in the class, the SingleClickListener is associated to the Button at stake
myButton = (Button) findViewById(R.id.my_button);
myButton.setOnClickListener(new SingleClickListener() {
@Override
public void performClick(View view) {
// do stuff
}
});
Section 43.6: Using the layout to define a click action
When we create a button in layout, we can use the android:onClick attribute to reference a method in code to
handle clicks.
Button
<Button
android:width="120dp"
android:height="wrap_content"
android:text="Click me"
GoalKicker.com – Android™ Notes for Professionals 339
android:onClick="handleClick" />
Then in your activity, create the handleClick method.
public void handleClick(View v) {
// Do whatever.
}
Section 43.7: Listening to the long click events
To catch a long click and use it you need to provide appropriate listener to button:
View.OnLongClickListener listener = new View.OnLongClickListener() {
public boolean onLongClick(View v) {
Button clickedButton = (Button) v;
String buttonText = clickedButton.getText().toString();
Log.v(TAG, "button long pressed --> " + buttonText);
return true;
}
};
button.setOnLongClickListener(listener);
GoalKicker.com – Android™ Notes for Professionals 340
Chapter 44: Emulator
Section 44.1: Taking screenshots
If you want to take a screenshot from the Android Emulator (2.0), then you just need to press Ctrl + S or you
click on the camera icon on the side bar:
GoalKicker.com – Android™ Notes for Professionals 341
GoalKicker.com – Android™ Notes for Professionals 342
If you use an older version of the Android Emulator or you want to take a screenshot from a real device, then you
need to click on the camera icon in the Android Monitor:
Double check that you have selected the right device, because this is a common pitfall.
After taking a screenshot, you can optionally add the following decorations to it (also see the image below):
1. A device frame around the screenshot.
2. A drop shadow below the device frame.
3. A screen glare across device frame and screenshot.
GoalKicker.com – Android™ Notes for Professionals 343
GoalKicker.com – Android™ Notes for Professionals 344
GoalKicker.com – Android™ Notes for Professionals 345
Section 44.2: Simulate call
To simulate a phone call, press the 'Extended controls' button indicated by three dots, choose 'Phone' and select
'Call'. You can also optionally change the phone number.
Section 44.3: Open the AVD Manager
Once the SDK installed, you can open the AVD Manager from the command line using android avd.
You can also access AVD Manager from Android studio using Tools > Android > AVD Manager or by clicking on the
AVD Manager icon in the toolbar which is the second in the screenshot below.
Section 44.4: Resolving Errors while starting emulator
First of all, ensure that you've enabled the 'Virtualization' in your BIOS setup.
Start the Android SDK Manager, select Extras and then select Intel Hardware Accelerated Execution Manager
and wait until your download completes. If it still doesn't work, open your SDK folder and run
/extras/intel/Hardware_Accelerated_Execution_Manager/IntelHAXM.exe.
Follow the on-screen instructions to complete installation.
Or for OS X you can do it without onscreen prompts like this:
/extras/intel/Hardware_Accelerated_Execution_Manager/HAXM\ installation
If your CPU does not support VT-x or SVM, you can not use x86-based Android images. Please use ARMbased
images instead.
After installation completed, confirm that the virtualization driver is operating correctly by opening a command
prompt window and running the following command: sc query intelhaxm
GoalKicker.com – Android™ Notes for Professionals 346
To run an x86-based emulator with VM acceleration: If you are running the emulator from the command line, just
specify an x86-based AVD: emulator -avd <avd_name>
If you follow all the steps mentioned above correctly, then surely you should be able to see your AVD with HAXM
coming up normally.
GoalKicker.com – Android™ Notes for Professionals 347
Chapter 45: Service
A Service runs in background to perform long-running operations or to perform work for remote processes. A
service does not provide any user interface it runs only in background with User’s input. For example a service can
play music in the background while the user is in a different App, or it might download data from the internet
without blocking user’s interaction with the Android device.
Section 45.1: Lifecycle of a Service
The services lifecycle has the following callbacks
onCreate() :
Executed when the service is first created in order to set up the initial configurations you might need. This method
is executed only if the service is not already running.
onStartCommand() :
Executed every time startService() is invoked by another component, like an Activity or a BroadcastReceiver.
When you use this method, the Service will run until you call stopSelf() or stopService(). Note that regardless of
how many times you call onStartCommand(), the methods stopSelf() and stopService() must be invoked only
once in order to stop the service.
onBind() :
Executed when a component calls bindService() and returns an instance of IBInder, providing a communication
channel to the Service. A call to bindService() will keep the service running as long as there are clients bound to it.
onDestroy() :
Executed when the service is no longer in use and allows for disposal of resources that have been allocated.
It is important to note that during the lifecycle of a service other callbacks might be invoked such as
onConfigurationChanged() and onLowMemory()
https://developer.android.com/guide/components/services.html
GoalKicker.com – Android™ Notes for Professionals 348
Section 45.2: Defining the process of a service
The android:process field defines the name of the process where the service is to run. Normally, all components of
an application run in the default process created for the application. However, a component can override the
default with its own process attribute, allowing you to spread your application across multiple processes.
If the name assigned to this attribute begins with a colon (':'), the service will run in its own separate process.
<service
android:name="com.example.appName"
android:process=":externalProcess" />
If the process name begins with a lowercase character, the service will run in a global process of that name,
provided that it has permission to do so. This allows components in different applications to share a process,
reducing resource usage.
Section 45.3: Creating an unbound service
The first thing to do is to add the service to AndroidManifest.xml, inside the <application> tag:
<application ...>
...
<service
android:name=".RecordingService"
GoalKicker.com – Android™ Notes for Professionals 349
<!--"enabled" tag specifies Whether or not the service can be instantiated by the system —
"true" -->
<!--if it can be, and "false" if not. The default value is "true".-->
android:enabled="true"
<!--exported tag specifies Whether or not components of other applications can invoke the -
->
<!--service or interact with it — "true" if they can, and "false" if not. When the value-
->
<!--is "false", only components of the same application or applications with the same user
-->
<!--ID can start the service or bind to it.-->
android:exported="false" />
</application>
If your intend to manage your service class in a separate package (eg: .AllServices.RecordingService) then you will
need to specify where your service is located. So, in above case we will modify:
android:name=".RecordingService"
to
android:name=".AllServices.RecordingService"
or the easiest way of doing so is to specify the full package name.
Then we create the actual service class:
public class RecordingService extends Service {
private int NOTIFICATION = 1; // Unique identifier for our notification
public static boolean isRunning = false;
public static RecordingService instance = null;
private NotificationManager notificationManager = null;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate(){
instance = this;
isRunning = true;
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId){
// The PendingIntent to launch our activity if the user selects this notification
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(this,
MainActivity.class), 0);
// Set the info for the views that show in the notification panel.
GoalKicker.com – Android™ Notes for Professionals 350
Notification notification = new NotificationCompat.Builder(this)
.setSmallIcon(R.mipmap.ic_launcher) // the status icon
.setTicker("Service running...") // the status text
.setWhen(System.currentTimeMillis()) // the time stamp
.setContentTitle("My App") // the label of the entry
.setContentText("Service running...") // the content of the entry
.setContentIntent(contentIntent) // the intent to send when the entry is
clicked
.setOngoing(true) // make persistent (disable swipe-away)
.build();
// Start service in foreground mode
startForeground(NOTIFICATION, notification);
return START_STICKY;
}
@Override
public void onDestroy(){
isRunning = false;
instance = null;
notificationManager.cancel(NOTIFICATION); // Remove notification
super.onDestroy();
}
public void doSomething(){
Toast.makeText(getApplicationContext(), "Doing stuff from service...",
Toast.LENGTH_SHORT).show();
}
}
All this service does is show a notification when it's running, and it can display toasts when its doSomething()
method is called.
As you'll notice, it's implemented as a singleton, keeping track of its own instance - but without the usual static
singleton factory method because services are naturally singletons and are created by intents. The instance is
useful to the outside to get a "handle" to the service when it's running.
Last, we need to start and stop the service from an activity:
public void startOrStopService(){
if( RecordingService.isRunning ){
// Stop service
Intent intent = new Intent(this, RecordingService.class);
stopService(intent);
}
else {
// Start service
Intent intent = new Intent(this, RecordingService.class);
startService(intent);
}
}
In this example, the service is started and stopped by the same method, depending on it's current state.
GoalKicker.com – Android™ Notes for Professionals 351
We can also invoke the doSomething() method from our activity:
public void makeServiceDoSomething(){
if( RecordingService.isRunning )
RecordingService.instance.doSomething();
}
Section 45.4: Starting a Service
Starting a service is very easy, just call startService with an intent, from within an Activity:
Intent intent = new Intent(this, MyService.class); //substitute MyService with the name of your
service
intent.putExtra(Intent.EXTRA_TEXT, "Some text"); //add any extra data to pass to the service
startService(intent); //Call startService to start the service.
Section 45.5: Creating Bound Service with help of Binder
Create a class which extends Service class and in overridden method onBind return your local binder instance:
public class LocalService extends Service {
// Binder given to clients
private final IBinder mBinder = new LocalBinder();
/**
* Class used for the client Binder. Because we know this service always
* runs in the same process as its clients, we don't need to deal with IPC.
*/
public class LocalBinder extends Binder {
LocalService getService() {
// Return this instance of LocalService so clients can call public methods
return LocalService.this;
}
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
Then in your activity bind to service in onStart callback, using ServiceConnection instance and unbind from it in
onStop:
public class BindingActivity extends Activity {
LocalService mService;
boolean mBound = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
@Override
protected void onStart() {
super.onStart();
GoalKicker.com – Android™ Notes for Professionals 352
// Bind to LocalService
Intent intent = new Intent(this, LocalService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onStop() {
super.onStop();
// Unbind from the service
if (mBound) {
unbindService(mConnection);
mBound = false;
}
}
/** Defines callbacks for service binding, passed to bindService() */
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className,
IBinder service) {
// We've bound to LocalService, cast the IBinder and get LocalService instance
LocalBinder binder = (LocalBinder) service;
mService = binder.getService();
mBound = true;
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
mBound = false;
}
};
}
Section 45.6: Creating Remote Service (via AIDL)
Describe your service access interface through .aidl file:
// IRemoteService.aidl
package com.example.android;
// Declare any non-default types here with import statements
/** Example service interface */
interface IRemoteService {
/** Request the process ID of this service, to do evil things with it. */
int getPid();
}
Now after build application, sdk tools will generate appropriate .java file. This file will contain Stub class which
implements our aidl interface, and which we need to extend:
public class RemoteService extends Service {
private final IRemoteService.Stub binder = new IRemoteService.Stub() {
@Override
public int getPid() throws RemoteException {
return Process.myPid();
}
};
GoalKicker.com – Android™ Notes for Professionals 353
@Nullable
@Override
public IBinder onBind(Intent intent) {
return binder;
}
}
Then in activity:
public class MainActivity extends AppCompatActivity {
private final ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
IRemoteService service = IRemoteService.Stub.asInterface(iBinder);
Toast.makeText(this, "Activity process: " + Process.myPid + ", Service process: " +
getRemotePid(service), LENGTH_SHORT).show();
}
@Override
public void onServiceDisconnected(ComponentName componentName) {}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
protected void onStart() {
super.onStart();
Intent intent = new Intent(this, RemoteService.class);
bindService(intent, connection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onStop() {
super.onStop();
unbindService(connection);
}
private int getRemotePid(IRemoteService service) {
int result = -1;
try {
result = service.getPid();
} catch (RemoteException e) {
e.printStackTrace();
}
return result;
}
}
GoalKicker.com – Android™ Notes for Professionals 354
Chapter 46: The Manifest File
The Manifest is an obligatory file named exactly "AndroidManifest.xml" and located in the app's root directory. It
specifies the app name, icon, Java package name, version, declaration of Activities, Services, app permissions and
other information.
Section 46.1: Declaring Components
The primary task of the manifest is to inform the system about the app's components. For example, a manifest file
can declare an activity as follows:
<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
<application android:icon="@drawable/app_icon.png" ... >
<activity android:name="com.example.project.ExampleActivity"
android:label="@string/example_label" ... >
</activity>
...
</application>
</manifest>
In the <application> element, the android:icon attribute points to resources for an icon that identifies the app.
In the element, the android:name attribute specifies the fully qualified class name of the Activity subclass and the
android:label attribute specifies a string to use as the user-visible label for the activity.
You must declare all app components this way:
-<activity> elements for activities
-<service> elements for services
-<receiver> elements for broadcast receivers
-<provider> elements for content providers
Activities, services, and content providers that you include in your source but do not declare in the manifest are not
visible to the system and, consequently, can never run. However, broadcast receivers can be either declared in the
manifest or created dynamically in code (as BroadcastReceiver objects) and registered with the system by calling
registerReceiver().
For more about how to structure the manifest file for your app, see The AndroidManifest.xml File documentation.
Section 46.2: Declaring permissions in your manifest file
Any permission required by your application to access a protected part of the API or to interact with other
applications must be declared in your AndroidManifest.xml file. This is done using the <uses-permission /> tag.
Syntax
<uses-permission android:name="string"
android:maxSdkVersion="integer"/>
android:name: This is the name of the required permission
GoalKicker.com – Android™ Notes for Professionals 355
android:maxSdkVersion: The highest API level at which this permission should be granted to your app. Setting this
permission is optional and should only be set if the permission your app requires is no longer needed at a certain
API level.
Sample AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.samplepackage">
<!-- request internet permission -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- request camera permission -->
<uses-permission android:name="android.permission.CAMERA"/>
<!-- request permission to write to external storage -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18" />
<application>....</application>
</manifest>
* Also see the Permissions topic.
GoalKicker.com – Android™ Notes for Professionals 356
Chapter 47: Gradle for Android
Gradle is a JVM-based build system that enables developers to write high-level scripts that can be used to automate
the process of compilation and application production. It is a flexible plugin-based system, which allows you to
automate various aspects of the build process; including compiling and signing a .jar, downloading and managing
external dependencies, injecting fields into the AndroidManifest or utilising specific SDK versions.
Section 47.1: A basic build.gradle file
This is an example of a default build.gradle file in a module.
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion '25.0.3'
signingConfigs {
applicationName {
keyAlias 'applicationName'
keyPassword 'password'
storeFile file('../key/applicationName.jks')
storePassword 'keystorePassword'
}
}
defaultConfig {
applicationId 'com.company.applicationName'
minSdkVersion 14
targetSdkVersion 25
versionCode 1
versionName '1.0'
signingConfig signingConfigs.applicationName
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support:design:25.3.1'
testCompile 'junit:junit:4.12'
}
DSL (domain-specific language)
Each block in the file above is called a DSL (domain-specific language).
Plugins
The first line, apply plugin: 'com.android.application', applies the Android plugin for Gradle to the build and
GoalKicker.com – Android™ Notes for Professionals 357
makes the android {} block available to declare Android-specific build options.
For an Android Application:
apply plugin: 'com.android.application'
For an Android Library:
apply plugin: 'com.android.library'
Understanding the DSLs in the sample above
The second part, The android {...} block, is the Android DSL which contains information about your project.
For example, you can set the compileSdkVersion which specifies the Android API level , Which should be used by
Gradle to compile your app.
The sub-block defaultConfig holds the defaults for your manifest. You can override them with Product Flavors.
You can find more info in these examples:
DSL for the app module
Build Types
Product Flavors
Signing settings
Dependencies
The dependencies block is defined outside the android block {...} : This means it's not defined by the Android
plugin but it's standard Gradle.
The dependencies block specifies what external libraries (typically Android libraries, but Java libraries are also valid)
you wish to include in your app. Gradle will automatically download these dependencies for you (if there is no local
copy available), you just need to add similar compile lines when you wish to add another library.
Let's look at one of the lines present here:
compile 'com.android.support:design:25.3.1'
This line basically says
add a dependency on the Android support design library to my project.
Gradle will ensure that the library is downloaded and present so that you can use it in your app, and its code will
also be included in your app.
If you're familiar with Maven, this syntax is the GroupId, a colon, ArtifactId, another colon, then the version of the
dependency you wish to include, giving you full control over versioning.
While it is possible to specify artifact versions using the plus (+) sign, best practice is to avoid doing so; it can lead to
issues if the library gets updated with breaking changes without your knowledge, which would likely lead to crashes
in your app.
GoalKicker.com – Android™ Notes for Professionals 358
You can add different kind of dependencies:
local binary dependencies
module dependencies
remote dependencies
A particular attention should be dedicated to the aar flat dependencies.
You can find more details in this topic.
Note about the -v7 in appcompat-v7
compile 'com.android.support:appcompat-v7:25.3.1'
This simply means that this library (appcompat) is compatible with the Android API level 7 and forward.
Note about the junit:junit:4.12
This is Testing dependency for Unit testing.
Specifying dependencies specific to different build configurations
You can specify that a dependency should only be used for a certain build configuration or you can define different
dependencies for the build types or the product flavors (e.g., debug, test or release) by using debugCompile,
testCompile or releaseCompile instead of the usual compile.
This is helpful for keeping test- and debug- related dependencies out of your release build, which will keep your
release APK as slim as possible and help to ensure that any debug information cannot be used to obtain internal
information about your app.
signingConfig
The signingConfig allows you to configure your Gradle to include keystore information and ensure that the APK
built using these configurations are signed and ready for Play Store release.
Here you can find a dedicated topic.
Note: It's not recommended though to keep the signing credentials inside your Gradle file. To remove the signing
configurations, just omit the signingConfigs portion.
You can specify them in different ways:
storing in an external file
storing them in setting environment variables.
See this topic for more details : Sign APK without exposing keystore password.
You can find further information about Gradle for Android in the dedicated Gradle topic.
Section 47.2: Define and use Build Configuration Fields
BuildConfigField
GoalKicker.com – Android™ Notes for Professionals 359
Gradle allows buildConfigField lines to define constants. These constants will be accessible at runtime as static
fields of the BuildConfig class. This can be used to create flavors by defining all fields within the defaultConfig
block, then overriding them for individual build flavors as needed.
This example defines the build date and flags the build for production rather than test:
android {
...
defaultConfig {
...
// defining the build date
buildConfigField "long", "BUILD_DATE", System.currentTimeMillis() + "L"
// define whether this build is a production build
buildConfigField "boolean", "IS_PRODUCTION", "false"
// note that to define a string you need to escape it
buildConfigField "String", "API_KEY", "\"my_api_key\""
}
productFlavors {
prod {
// override the productive flag for the flavor "prod"
buildConfigField "boolean", "IS_PRODUCTION", "true"
resValue 'string', 'app_name', 'My App Name'
}
dev {
// inherit default fields
resValue 'string', 'app_name', 'My App Name - Dev'
}
}
}
The automatically-generated <package_name>.BuildConfig.java in the gen folder contains the following fields
based on the directive above:
public class BuildConfig {
// ... other generated fields ...
public static final long BUILD_DATE = 1469504547000L;
public static final boolean IS_PRODUCTION = false;
public static final String API_KEY = "my_api_key";
}
The defined fields can now be used within the app at runtime by accessing the generated BuildConfig class:
public void example() {
// format the build date
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");
String buildDate = dateFormat.format(new Date(BuildConfig.BUILD_DATE));
Log.d("build date", buildDate);
// do something depending whether this is a productive build
if (BuildConfig.IS_PRODUCTION) {
connectToProductionApiEndpoint();
} else {
connectToStagingApiEndpoint();
}
}
ResValue
GoalKicker.com – Android™ Notes for Professionals 360
The resValue in the productFlavors creates a resource value. It can be any type of resource (string, dimen, color,
etc.). This is similar to defining a resource in the appropriate file: e.g. defining string in a strings.xml file. The
advantage being that the one defined in gradle can be modified based on your productFlavor/buildVariant. To
access the value, write the same code as if you were accessing a res from the resources file:
getResources().getString(R.string.app_name)
The important thing is that resources defined this way cannot modify existing resources defined in files. They can
only create new resource values.
Some libraries (such as the Google Maps Android API) require an API key provided in the Manifest as a meta-data
tag. If different keys are needed for debugging and production builds, specify a manifest placeholder filled in by
Gradle.
In your AndroidManifest.xml file:
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}"/>
And then set the field accordingly in your build.gradle file:
android {
defaultConfig {
...
// Your development key
manifestPlaceholders = [ MAPS_API_KEY: "AIza..." ]
}
productFlavors {
prod {
// Your production key
manifestPlaceholders = [ MAPS_API_KEY: "AIza..." ]
}
}
}
The Android build system generates a number of fields automatically and places them in BuildConfig.java. These
fields are:
Field Description
DEBUG a Boolean stating if the app is in debug or release mode
APPLICATION_ID a String containing the ID of the application (e.g. com.example.app)
BUILD_TYPE a String containing the build type of the application (usually either debug or release)
FLAVOR a String containing the particular flavor of the build
VERSION_CODE an int containing the version (build) number.
This is the same as versionCode in build.gradle or versionCode in AndroidManifest.xml
VERSION_NAME a String containing the version (build) name.
This is the same as versionName in build.gradle or versionName in AndroidManifest.xml
In addition to the above, if you have defined multiple dimensions of flavor then each dimension will have its own
value. For example, if you had two dimensions of flavor for color and size you will also have the following
variables:
Field Description
GoalKicker.com – Android™ Notes for Professionals 361
FLAVOR_color a String containing the value for the 'color' flavor.
FLAVOR_size a String containing the value for the 'size' flavor.
Section 47.3: Centralizing dependencies via
"dependencies.gradle" file
When working with multi-module projects, it is helpful to centralize dependencies in a single location rather than
having them spread across many build files, especially for common libraries such as the Android support libraries
and the Firebase libraries.
One recommended way is to separate the Gradle build files, with one build.gradle per module, as well as one in
the project root and another one for the dependencies, for example:
root
+- gradleScript/
| dependencies.gradle
+- module1/
| build.gradle
+- module2/
| build.gradle
+- build.gradle
Then, all of your dependencies can be located in gradleScript/dependencies.gradle:
ext {
// Version
supportVersion = '24.1.0'
// Support Libraries dependencies
supportDependencies = [
design: "com.android.support:design:${supportVersion}",
recyclerView: "com.android.support:recyclerview-v7:${supportVersion}",
cardView: "com.android.support:cardview-v7:${supportVersion}",
appCompat: "com.android.support:appcompat-v7:${supportVersion}",
supportAnnotation: "com.android.support:support-annotations:${supportVersion}",
]
firebaseVersion = '9.2.0';
firebaseDependencies = [
core: "com.google.firebase:firebase-core:${firebaseVersion}",
database: "com.google.firebase:firebase-database:${firebaseVersion}",
storage: "com.google.firebase:firebase-storage:${firebaseVersion}",
crash: "com.google.firebase:firebase-crash:${firebaseVersion}",
auth: "com.google.firebase:firebase-auth:${firebaseVersion}",
messaging: "com.google.firebase:firebase-messaging:${firebaseVersion}",
remoteConfig: "com.google.firebase:firebase-config:${firebaseVersion}",
invites: "com.google.firebase:firebase-invites:${firebaseVersion}",
adMod: "com.google.firebase:firebase-ads:${firebaseVersion}",
appIndexing: "com.google.android.gms:play-services-appindexing:${firebaseVersion}",
];
}
Which can then be applied from that file in the top level file build.gradle like so:
// Load dependencies
apply from: 'gradleScript/dependencies.gradle'
GoalKicker.com – Android™ Notes for Professionals 362
and in the module1/build.gradle like so:
// Module build file
dependencies {
// ...
compile supportDependencies.appCompat
compile supportDependencies.design
compile firebaseDependencies.crash
}
Another approach
A less verbose approach for centralizing library dependencies versions can be achieved by declaring the version
number as a variable once, and using it everywhere.
In the workspace root build.gradle add this:
ext.v = [
supportVersion:'24.1.1',
]
And in every module that uses the same library add the needed libraries
compile "com.android.support:support-v4:${v.supportVersion}"
compile "com.android.support:recyclerview-v7:${v.supportVersion}"
compile "com.android.support:design:${v.supportVersion}"
compile "com.android.support:support-annotations:${v.supportVersion}"
Section 47.4: Sign APK without exposing keystore password
You can define the signing configuration to sign the apk in the build.gradle file using these properties:
storeFile : the keystore file
storePassword: the keystore password
keyAlias: a key alias name
keyPassword: A key alias password
In many case you may need to avoid this kind of info in the build.gradle file.
Method A: Configure release signing using a keystore.properties file
It's possible to configure your app's build.gradle so that it will read your signing configuration information from a
properties file like keystore.properties.
Setting up signing like this is beneficial because:
Your signing configuration information is separate from your build.gradle file
You do not have to intervene during the signing process in order to provide passwords for your keystore file
You can easily exclude the keystore.properties file from version control
First, create a file called keystore.properties in the root of your project with content like this (replacing the values
with your own):
storeFile=keystore.jks
storePassword=storePassword
keyAlias=keyAlias
GoalKicker.com – Android™ Notes for Professionals 363
keyPassword=keyPassword
Now, in your app's build.gradle file, set up the signingConfigs block as follows:
android {
...
signingConfigs {
release {
def propsFile = rootProject.file('keystore.properties')
if (propsFile.exists()) {
def props = new Properties()
props.load(new FileInputStream(propsFile))
storeFile = file(props['storeFile'])
storePassword = props['storePassword']
keyAlias = props['keyAlias']
keyPassword = props['keyPassword']
}
}
}
}
That's really all there is to it, but don't forget to exclude both your keystore file and your keystore.properties
file from version control.
A couple of things to note:
The storeFile path specified in the keystore.properties file should be relative to your app's build.gradle
file. This example assumes that the keystore file is in the same directory as the app's build.gradle file.
This example has the keystore.properties file in the root of the project. If you put it somewhere else, be
sure to change the value in rootProject.file('keystore.properties') to the location of yours, relative to
the root of your project.
Method B: By using an environment variable
The same can be achieved also without a properties file, making the password harder to find:
android {
signingConfigs {
release {
storeFile file('/your/keystore/location/key')
keyAlias 'your_alias'
String ps = System.getenv("ps")
if (ps == null) {
throw new GradleException('missing ps env variable')
}
keyPassword ps
storePassword ps
}
}
The "ps" environment variable can be global, but a safer approach can be by adding it to the shell of Android
Studio only.
In linux this can be done by editing Android Studio's Desktop Entry
Exec=sh -c "export ps=myPassword123 ; /path/to/studio.sh"
GoalKicker.com – Android™ Notes for Professionals 364
You can find more details in this topic.
Section 47.5: Adding product flavor-specific dependencies
Dependencies can be added for a specific product flavor, similar to how they can be added for specific build
configurations.
For this example, assume that we have already defined two product flavors called free and paid (more on defining
flavors here).
We can then add the AdMob dependency for the free flavor, and the Picasso library for the paid one like so:
android {
...
productFlavors {
free {
applicationId "com.example.app.free"
versionName "1.0-free"
}
paid {
applicationId "com.example.app.paid"
versionName "1.0-paid"
}
}
}
...
dependencies {
...
// Add AdMob only for free flavor
freeCompile 'com.android.support:appcompat-v7:23.1.1'
freeCompile 'com.google.android.gms:play-services-ads:8.4.0'
freeCompile 'com.android.support:support-v4:23.1.1'
// Add picasso only for paid flavor
paidCompile 'com.squareup.picasso:picasso:2.5.2'
}
...
Section 47.6: Specifying dierent application IDs for build
types and product flavors
You can specify different application IDs or package names for each buildType or productFlavor using the
applicationIdSuffix configuration attribute:
Example of suffixing the applicationId for each buildType:
defaultConfig {
applicationId "com.package.android"
minSdkVersion 17
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
debuggable false
GoalKicker.com – Android™ Notes for Professionals 365
}
development {
debuggable true
applicationIdSuffix ".dev"
}
testing {
debuggable true
applicationIdSuffix ".qa"
}
}
Our resulting applicationIds would now be:
com.package.android for release
com.package.android.dev for development
com.package.android.qa for testing
This can be done for productFlavors as well:
productFlavors {
free {
applicationIdSuffix ".free"
}
paid {
applicationIdSuffix ".paid"
}
}
The resulting applicationIds would be:
com.package.android.free for the free flavor
com.package.android.paid for the paid flavor
Section 47.7: Versioning your builds via "version.properties"
file
You can use Gradle to auto-increment your package version each time you build it. To do so create a
version.properties file in the same directory as your build.gradle with the following contents:
VERSION_MAJOR=0
VERSION_MINOR=1
VERSION_BUILD=1
(Changing the values for major and minor as you see fit). Then in your build.gradle add the following code to the
android section:
// Read version information from local file and increment as appropriate
def versionPropsFile = file('version.properties')
if (versionPropsFile.canRead()) {
def Properties versionProps = new Properties()
versionProps.load(new FileInputStream(versionPropsFile))
def versionMajor = versionProps['VERSION_MAJOR'].toInteger()
def versionMinor = versionProps['VERSION_MINOR'].toInteger()
GoalKicker.com – Android™ Notes for Professionals 366
def versionBuild = versionProps['VERSION_BUILD'].toInteger() + 1
// Update the build number in the local file
versionProps['VERSION_BUILD'] = versionBuild.toString()
versionProps.store(versionPropsFile.newWriter(), null)
defaultConfig {
versionCode versionBuild
versionName "${versionMajor}.${versionMinor}." + String.format("%05d", versionBuild)
}
}
The information can be accessed in Java as a string BuildConfig.VERSION_NAME for the complete
{major}.{minor}.{build} number and as an integer BuildConfig.VERSION_CODE for just the build number.
Section 47.8: Defining product flavors
Product flavors are defined in the build.gradle file inside the android { ... } block as seen below.
...
android {
...
productFlavors {
free {
applicationId "com.example.app.free"
versionName "1.0-free"
}
paid {
applicationId "com.example.app.paid"
versionName "1.0-paid"
}
}
}
By doing this, we now have two additional product flavors: free and paid. Each can have its own specific
configuration and attributes. For example, both of our new flavors has a separate applicationId and versionName
than our existing main flavor (available by default, so not shown here).
Section 47.9: Changing output apk name and add version
name:
This is the code for changing output application file name (.apk). The name can be configured by assigning a
different value to newName
android {
applicationVariants.all { variant ->
def newName = "ApkName";
variant.outputs.each { output ->
def apk = output.outputFile;
newName += "-v" + defaultConfig.versionName;
if (variant.buildType.name == "release") {
newName += "-release.apk";
} else {
newName += ".apk";
}
if (!output.zipAlign) {
GoalKicker.com – Android™ Notes for Professionals 367
newName = newName.replace(".apk", "-unaligned.apk");
}
output.outputFile = new File(apk.parentFile, newName);
logger.info("INFO: Set outputFile to "
+ output.outputFile
+ " for [" + output.name + "]");
}
}
}
Section 47.10: Adding product flavor-specific resources
Resources can be added for a specific product flavor.
For this example, assume that we have already defined two product flavors called free and paid. In order to add
product flavor-specific resources, we create additional resource folders alongside the main/res folder, which we
can then add resources to like usual. For this example, we'll define a string, status, for each product flavor:
/src/main/res/values/strings.xml
<resources>
<string name="status">Default</string>
</resources>
/src/free/res/values/strings.xml
<resources>
<string name="status">Free</string>
</resources>
/src/paid/res/values/strings.xml
<resources>
<string name="status">Paid</string>
</resources>
The product flavor-specific status strings will override the value for status in the main flavor.
Section 47.11: Why are there two build.gradle files in an
Android Studio project?
<PROJECT_ROOT>\app\build.gradle is specific for app module.
<PROJECT_ROOT>\build.gradle is a "Top-level build file" where you can add configuration options common to all
sub-projects/modules.
If you use another module in your project, as a local library you would have another build.gradle file:
<PROJECT_ROOT>\module\build.gradle
In the top level file you can specify common properties as the buildscript block or some common properties.
buildscript {
repositories {
mavenCentral()
}
GoalKicker.com – Android™ Notes for Professionals 368
dependencies {
classpath 'com.android.tools.build:gradle:2.2.0'
classpath 'com.google.gms:google-services:3.0.0'
}
}
ext {
compileSdkVersion = 23
buildToolsVersion = "23.0.1"
}
In the app\build.gradle you define only the properties for the module:
apply plugin: 'com.android.application'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
}
dependencies {
//.....
}
Section 47.12: Directory structure for flavor-specific
resources
Different flavors of application builds can contain different resources. To create a flavor-specific resource make a
directory with the lower-case name of your flavor in the src directory and add your resources in the same way you
would normally.
For example, if you had a flavour Development and wanted to provide a distinct launcher icon for it you would
create a directory src/development/res/drawable-mdpi and inside that directory create an ic_launcher.png file
with your development-specific icon.
The directory structure will look like this:
src/
main/
res/
drawable-mdpi/
ic_launcher.png <-- the default launcher icon
development/
res/
drawable-mdpi/
ic_launcher.png <-- the launcher icon used when the product flavor is 'Development'
(Of course, in this case you would also create icons for drawable-hdpi, drawable-xhdpi etc).
Section 47.13: Enable Proguard using gradle
For enabling Proguard configurations for your application you need to enable it in your module-level gradle file. You
need to set the value of minifyEnabled to true.
buildTypes {
release {
GoalKicker.com – Android™ Notes for Professionals 369
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
The above code will apply your Proguard configurations contained in the default Android SDK combined with the
"proguard-rules.pro" file on your module to your released apk.
Section 47.14: Ignoring build variant
For some reasons you may want to ignore your build variants. For example: you have 'mock' product flavour and
you use it only for debug purposes, such as unit/instrumentation tests.
Let's ignore mockRelease variant from our project. Open build.gradle file and write:
// Remove mockRelease as it's not needed.
android.variantFilter { variant ->
if (variant.buildType.name.equals('release') &&
variant.getFlavors().get(0).name.equals('mock')) {
variant.setIgnore(true);
}
}
Section 47.15: Enable experimental NDK plugin support for
Gradle and AndroidStudio
Enable and configure the experimental Gradle plugin to improve AndroidStudio's NDK support. Check that you
fulfill the following requirements:
Gradle 2.10 (for this example)
Android NDK r10 or later
Android SDK with build tools v19.0.0 or later
Configure MyApp/build.gradle file
Edit the dependencies.classpath line in build.gradle from e.g.
classpath 'com.android.tools.build:gradle:2.1.2'
to
classpath 'com.android.tools.build:gradle-experimental:0.7.2'
(v0.7.2 was the latest version at the time of writing. Check the latest version yourself and adapt your line
accordingly)
The build.gradle file should look similar to this:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle-experimental:0.7.2'
}
}
GoalKicker.com – Android™ Notes for Professionals 370
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Configure MyApp/app/build.gradle file
Edit the build.gradle file to look similar to the following example. Your version numbers may look different.
apply plugin: 'com.android.model.application'
model {
android {
compileSdkVersion 19
buildToolsVersion "24.0.1"
defaultConfig {
applicationId "com.example.mydomain.myapp"
minSdkVersion.apiLevel 19
targetSdkVersion.apiLevel 19
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles.add(file('proguard-android.txt'))
}
}
ndk {
moduleName "myLib"
/* The following lines are examples of a some optional flags that
you may set to configure your build environment
*/
cppFlags.add("-I${file("path/to/my/includes/dir")}".toString())
cppFlags.add("-std=c++11")
ldLibs.addAll(['log', 'm'])
stl = "c++_static"
abiFilters.add("armeabi-v7a")
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
}
Sync and check that there are no errors in the Gradle files before proceeding.
Test if plugin is enabled
First make sure you have downloaded the Android NDK module. Then create an new app in AndroidStudio and add
the following to the ActivityMain file:
public class MainActivity implements Activity {
GoalKicker.com – Android™ Notes for Professionals 371
onCreate() {
// Pregenerated code. Not important here
}
static {
System.loadLibrary("myLib");
}
public static native String getString();
}
The getString() part should be highlighted red saying that the corresponding JNI function could not be found.
Hover your mouse over the function call until a red lightbulb appears. Click the bulb and select create function
JNI_.... This should generate a myLib.c file in the myApp/app/src/main/jni directory with the correct JNI function
call. It should look similar to this:
#include <jni.h>
JNIEXPORT jstring JNICALL
Java_com_example_mydomain_myapp_MainActivity_getString(JNIEnv *env, jobject instance)
{
// TODO
return (*env)->NewStringUTF(env, returnValue);
}
If it doesn't look like this, then the plugin has not correctly been configured or the NDK has not been downloaded
Section 47.16: Display signing information
In some circumstances (for example obtaining a Google API key) you need to find your keystore fingerprint. Gradle
has a convenient task that display all the signing information, including keystore fingerprints:
./gradlew signingReport
This is a sample output:
:app:signingReport
Variant: release
Config: none
----------
Variant: debug
Config: debug
Store: /Users/user/.android/debug.keystore
Alias: AndroidDebugKey
MD5: 25:08:76:A9:7C:0C:19:35:99:02:7B:00:AA:1E:49:CA
SHA1: 26:BE:89:58:00:8C:5A:7D:A3:A9:D3:60:4A:30:53:7A:3D:4E:05:55
Valid until: Saturday 18 June 2044
----------
Variant: debugAndroidTest
Config: debug
Store: /Users/user/.android/debug.keystore
Alias: AndroidDebugKey
MD5: 25:08:76:A9:7C:0C:19:35:99:02:7B:00:AA:1E:49:CA
SHA1: 26:BE:89:58:00:8C:5A:7D:A3:A9:D3:60:4A:30:53:7A:3D:4E:05:55
Valid until: Saturday 18 June 2044
----------
Variant: debugUnitTest
Config: debug
Store: /Users/user/.android/debug.keystore
Alias: AndroidDebugKey
GoalKicker.com – Android™ Notes for Professionals 372
MD5: 25:08:76:A9:7C:0C:19:35:99:02:7B:00:AA:1E:49:CA
SHA1: 26:BE:89:58:00:8C:5A:7D:A3:A9:D3:60:4A:30:53:7A:3D:4E:05:55
Valid until: Saturday 18 June 2044
----------
Variant: releaseUnitTest
Config: none
----------
Section 47.17: Seeing dependency tree
Use the task dependencies. Depending on how your modules are set up, it may be either ./gradlew dependencies
or to see the dependencies of module app use ./gradlew :app:dependencies
The example following build.gradle file
dependencies {
compile 'com.android.support:design:23.2.1'
compile 'com.android.support:cardview-v7:23.1.1'
compile 'com.google.android.gms:play-services:6.5.87'
}
will produce the following graph:
Parallel execution is an incubating feature.
:app:dependencies
------------------------------------------------------------
Project :app
------------------------------------------------------------
. . .
_releaseApk - ## Internal use, do not manually configure ##
+--- com.android.support:design:23.2.1
| +--- com.android.support:support-v4:23.2.1
| | \--- com.android.support:support-annotations:23.2.1
| +--- com.android.support:appcompat-v7:23.2.1
| | +--- com.android.support:support-v4:23.2.1 (*)
| | +--- com.android.support:animated-vector-drawable:23.2.1
| | | \--- com.android.support:support-vector-drawable:23.2.1
| | | \--- com.android.support:support-v4:23.2.1 (*)
| | \--- com.android.support:support-vector-drawable:23.2.1 (*)
| \--- com.android.support:recyclerview-v7:23.2.1
| +--- com.android.support:support-v4:23.2.1 (*)
| \--- com.android.support:support-annotations:23.2.1
+--- com.android.support:cardview-v7:23.1.1
\--- com.google.android.gms:play-services:6.5.87
\--- com.android.support:support-v4:21.0.0 -> 23.2.1 (*)
. . .
Here you can see the project is directly including com.android.support:design version 23.2.1, which itself is
bringing com.android.support:support-v4 with version 23.2.1. However, com.google.android.gms:playservices
itself has a dependency on the same support-v4 but with an older version 21.0.0, which is a conflict
detected by gradle.
(*) are used when gradle skips the subtree because those dependencies were already listed previously.
GoalKicker.com – Android™ Notes for Professionals 373
Section 47.18: Disable image compression for a smaller APK
file size
If you are optimizing all images manually, disable APT Cruncher for a smaller APK file size.
android {
aaptOptions {
cruncherEnabled = false
}
}
Section 47.19: Delete "unaligned" apk automatically
If you don't need automatically generated apk files with unaligned suffix (which you probably don't), you may add
the following code to build.gradle file:
// delete unaligned files
android.applicationVariants.all { variant ->
variant.assemble.doLast {
variant.outputs.each { output ->
println "aligned " + output.outputFile
println "unaligned " + output.packageApplication.outputFile
File unaligned = output.packageApplication.outputFile;
File aligned = output.outputFile
if (!unaligned.getName().equalsIgnoreCase(aligned.getName())) {
println "deleting " + unaligned.getName()
unaligned.delete()
}
}
}
}
From here
Section 47.20: Executing a shell script from gradle
A shell script is a very versatile way to extend your build to basically anything you can think of.
As an example, here is a simple script to compile protobuf files and add the result java files to the source directory
for further compilation:
def compilePb() {
exec {
// NOTICE: gradle will fail if there's an error in the protoc file...
executable "../pbScript.sh"
}
}
project.afterEvaluate {
compilePb()
}
The 'pbScript.sh' shell script for this example, located in the project's root folder:
#!/usr/bin/env bash
GoalKicker.com – Android™ Notes for Professionals 374
pp=/home/myself/my/proto
/usr/local/bin/protoc -I=$pp \
--java_out=./src/main/java \
--proto_path=$pp \
$pp/my.proto \
--proto_path=$pp \
$pp/my_other.proto
Section 47.21: Show all gradle project tasks
gradlew tasks -- show all tasks
Android tasks
-------------
androidDependencies - Displays the Android dependencies of the project.
signingReport - Displays the signing info for each variant.
sourceSets - Prints out all the source sets defined in this project.
Build tasks
-----------
assemble - Assembles all variants of all applications and secondary packages.
assembleAndroidTest - Assembles all the Test applications.
assembleDebug - Assembles all Debug builds.
assembleRelease - Assembles all Release builds.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles main classes.
clean - Deletes the build directory.
compileDebugAndroidTestSources
compileDebugSources
compileDebugUnitTestSources
compileReleaseSources
compileReleaseUnitTestSources
extractDebugAnnotations - Extracts Android annotations for the debug variant into the archive file
extractReleaseAnnotations - Extracts Android annotations for the release variant into the archive
file
jar - Assembles a jar archive containing the main classes.
mockableAndroidJar - Creates a version of android.jar that is suitable for unit tests.
testClasses - Assembles test classes.
Build Setup tasks
-----------------
init - Initializes a new Gradle build. [incubating]
wrapper - Generates Gradle wrapper files. [incubating]
Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.
Help tasks
----------
buildEnvironment - Displays all buildscript dependencies declared in root project 'LeitnerBoxPro'.
components - Displays the components produced by root project 'LeitnerBoxPro'. [incubating]
dependencies - Displays all dependencies declared in root project 'LeitnerBoxPro'.
dependencyInsight - Displays the insight into a specific dependency in root project
'LeitnerBoxPro'.
help - Displays a help message.
GoalKicker.com – Android™ Notes for Professionals 375
model - Displays the configuration model of root project 'LeitnerBoxPro'. [incubating]
projects - Displays the sub-projects of root project 'LeitnerBoxPro'.
properties - Displays the properties of root project 'LeitnerBoxPro'.
tasks - Displays the tasks runnable from root project 'LeitnerBoxPro' (some of the displayed tasks
may belong to subprojects)
.
Install tasks
-------------
installDebug - Installs the Debug build.
installDebugAndroidTest - Installs the android (on device) tests for the Debug build.
uninstallAll - Uninstall all applications.
uninstallDebug - Uninstalls the Debug build.
uninstallDebugAndroidTest - Uninstalls the android (on device) tests for the Debug build.
uninstallRelease - Uninstalls the Release build.
Verification tasks
------------------
check - Runs all checks.
connectedAndroidTest - Installs and runs instrumentation tests for all flavors on connected
devices.
connectedCheck - Runs all device checks on currently connected devices.
connectedDebugAndroidTest - Installs and runs the tests for debug on connected devices.
deviceAndroidTest - Installs and runs instrumentation tests using all Device Providers.
deviceCheck - Runs all device checks using Device Providers and Test Servers.
lint - Runs lint on all variants.
lintDebug - Runs lint on the Debug build.
lintRelease - Runs lint on the Release build.
test - Run unit tests for all variants.
testDebugUnitTest - Run unit tests for the debug build.
testReleaseUnitTest - Run unit tests for the release build.
Other tasks
-----------
assembleDefault
clean
jarDebugClasses
jarReleaseClasses
transformResourcesWithMergeJavaResForDebugUnitTest
transformResourcesWithMergeJavaResForReleaseUnitTest
Section 47.22: Debugging your Gradle errors
The following is an excerpt from Gradle - What is a non-zero exit value and how do I fix it?, see it for the full
discussion.
Let's say you are developing an application and you get some Gradle error that appears that generally will look like
so.
:module:someTask FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':module:someTask'.
> some message here... finished with non-zero exit value X
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more
log output.
BUILD FAILED
Total time: Y.ZZ secs
GoalKicker.com – Android™ Notes for Professionals 376
You search here on StackOverflow for your problem, and people say to clean and rebuild your project, or enable
MultiDex, and when you try that, it just isn't fixing the problem.
There are ways to get more information, but the Gradle output itself should point at the actual error in the few lines
above that message between :module:someTask FAILED and the last :module:someOtherTask that passed.
Therefore, if you ask a question about your error, please edit your questions to include more context to the error.
So, you get a "non-zero exit value." Well, that number is a good indicator of what you should try to fix. Here are a
few occur most frequently.
1 is a just a general error code and the error is likely in the Gradle output
2 seems to be related to overlapping dependencies or project misconfiguration.
3 seems to be from including too many dependencies, or a memory issue.
The general solutions for the above (after attempting a Clean and Rebuild of the project) are:
1 - Address the error that is mentioned. Generally, this is a compile-time error, meaning some piece of code
in your project is not valid. This includes both XML and Java for an Android project.
2 & 3 - Many answers here tell you to enable multidex. While it may fix the problem, it is most likely a
workaround. If you don't understand why you are using it (see the link), you probably don't need it. General
solutions involve cutting back your overuse of library dependencies (such as all of Google Play Services, when
you only need to use one library, like Maps or Sign-In, for example).
Section 47.23: Use gradle.properties for central
versionnumber/buildconfigurations
You can define central config info's in
a separate gradle include file Centralizing dependencies via "dependencies.gradle" file
a stand alone properties file Versioning your builds via "version.properties" file
or do it with root gradle.properties file
the project structure
root
+- module1/
| build.gradle
+- module2/
| build.gradle
+- build.gradle
+- gradle.properties
global setting for all submodules in gradle.properties
# used for manifest
# todo increment for every release
appVersionCode=19
appVersionName=0.5.2.160726
# android tools settings
appCompileSdkVersion=23
appBuildToolsVersion=23.0.2
usage in a submodule
GoalKicker.com – Android™ Notes for Professionals 377
apply plugin: 'com.android.application'
android {
// appXXX are defined in gradle.properties
compileSdkVersion = Integer.valueOf(appCompileSdkVersion)
buildToolsVersion = appBuildToolsVersion
defaultConfig {
// appXXX are defined in gradle.properties
versionCode = Long.valueOf(appVersionCode)
versionName = appVersionName
}
}
dependencies {
...
}
Note: If you want to publish your app in the F-Droid app store you have to use magic numbers in the gradle file
because else f-droid robot cannot read current versionnumner to detect/verify version changes.
Section 47.24: Defining build types
You can create and configure build types in the module-level build.gradle file inside the android {} block.
android {
...
defaultConfig {...}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
applicationIdSuffix ".debug"
}
}
}
GoalKicker.com – Android™ Notes for Professionals 378
Chapter 48: FileIO with Android
Reading and writing files in Android are not different from reading and writing files in standard Java. Same java.io
package can be used. However, there is some specific related to the folders where you are allowed to write,
permissions in general and MTP work arounds.
Section 48.1: Obtaining the working folder
You can get your working folder by calling the method getFilesDir() on your Activity (Activity is the central class in
your application that inherits from Context. See here). Reading is not different. Only your application will have
access to this folder.
Your activity could contain the following code, for instance:
File myFolder = getFilesDir();
File myFile = new File(myFolder, "myData.bin");
Section 48.2: Writing raw array of bytes
File myFile = new File(getFilesDir(), "myData.bin");
FileOutputStream out = new FileOutputStream(myFile);
// Write four bytes one two three four:
out.write(new byte [] { 1, 2, 3, 4}
out.close()
There is nothing Android specific with this code. If you write lots of small values often, use BufferedOutputStream
to reduce the wear of the device internal SSD.
Section 48.3: Serializing the object
The old good Java object serialization is available for you in Android. you can define Serializable classes like:
class Cirle implements Serializable {
final int radius;
final String name;
Circle(int radius, int name) {
this.radius = radius;
this.name = name;
}
}
and then write then to the ObjectOutputStream:
File myFile = new File(getFilesDir(), "myObjects.bin");
FileOutputStream out = new FileOutputStream(myFile);
ObjectOutputStream oout = new ObjectOutputStream(new BufferedOutputStream(out));
oout.writeObject(new Circle(10, "One"));
oout.writeObject(new Circle(12, "Two"));
oout.close()
Java object serialization may be either perfect or really bad choice, depending on what do you want to do with it -
GoalKicker.com – Android™ Notes for Professionals 379
outside the scope of this tutorial and sometimes opinion based. Read about the versioning first if you decide to use
it.
Section 48.4: Writing to external storage (SD card)
You can also read and write from/to memory card (SD card) that is present in many Android devices. Files in this
location can be accessed by other programs, also directly by the user after connecting device to PC via USB cable
and enabling MTP protocol.
Finding the SD card location is somewhat more problematic. The Environment class contains static methods to get
"external directories" that should normally be inside the SD card, also information if the SD card exists at all and is
writable. This question contains valuable answers how to make sure the right location will be found.
Accessing external storage requires permissions in you Android manifest:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
For older versions of Android putting permissions it is enough to put these permissions into manifest (the user
must approve during installation). However starting from Android 6.0 Android asks the user for approval at the
time of the first access, and you must support this new approach. Otherwise access is denied regardless of your
manifest.
In Android 6.0, first you need to check for permission, then, if not granted, request it. The code examples can be
found inside this SO question.
Section 48.5: Solving "Invisible MTP files" problem
If you create files for exporting via USB cable to desktop using MTP protocol, may be a problem that newly created
files are not immediately visible in the file explorer running on the connected desktop PC. To to make new files
visible, you need to call MediaScannerConnection:
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOCUMENTS), "theDocument.txt");
FileOutputStream out = new FileOutputStream(file)
... (write the document)
out.close()
MediaScannerConnection.scanFile(this, new String[] {file.getPath()}, null, null);
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
Uri.fromFile(file)));
This MediaScannerConnection call code works for files only, not for directories. The problem is described in this
Android bug report. This may be fixed for some version in the future, or on some devices.
Section 48.6: Working with big files
Small files are processed in a fraction of second and you can read / write them in place of the code where you need
this. However if the file is bigger or otherwise slower to process, you may need to use AsyncTask in Android to work
with the file in the background:
class FileOperation extends AsyncTask<String, Void, File> {
@Override
GoalKicker.com – Android™ Notes for Professionals 380
protected File doInBackground(String... params) {
try {
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOCUMENTS), "bigAndComplexDocument.odf");
FileOutputStream out = new FileOutputStream(file)
... (write the document)
out.close()
return file;
} catch (IOException ex) {
Log.e("Unable to write", ex);
return null;
}
}
@Override
protected void onPostExecute(File result) {
// This is called when we finish
}
@Override
protected void onPreExecute() {
// This is called before we begin
}
@Override
protected void onProgressUpdate(Void... values) {
// Unlikely required for this example
}
}
}
and then
new FileOperation().execute("Some parameters");
This SO question contains the complete example on how to create and call the AsyncTask. Also see the question on
error handling on how to handle IOExceptions and other errors.
GoalKicker.com – Android™ Notes for Professionals 381
Chapter 49: FileProvider
Section 49.1: Sharing a file
In this example you'll learn how to share a file with other apps. We'll use a pdf file in this example although the
code works with every other format as well.
The roadmap:
Specify the directories in which the files you want to share are placed
To share files we'll use a FileProvider, a class allowing secure file sharing between apps. A FileProvider can only
share files in predefined directories, so let's define these.
1. Create a new XML file that will contain the paths, e.g. res/xml/filepaths.xml
2. Add the paths
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="pdf_folder" path="documents/"/>
</paths>
Define a FileProvider and link it with the file paths
This is done in the manifest:
<manifest>
...
<application>
...
<provider
android:name="android.support.v4.context.FileProvider"
android:authorities="com.mydomain.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
...
</application>
...
</manifest>
Generate the URI for the file
To share the file we must provide an identifier for the file. This is done by using a URI (Uniform Resource Identifier).
// We assume the file we want to load is in the documents/ subdirectory
// of the internal storage
File documentsPath = new File(Context.getFilesDir(), "documents");
File file = new File(documentsPath, "sample.pdf");
// This can also in one line of course:
// File file = new File(Context.getFilesDir(), "documents/sample.pdf");
Uri uri = FileProvider.getUriForFile(getContext(), "com.mydomain.fileprovider", file);
As you can see in the code we first make a new File class representing the file. To get a URI we ask FileProvider to
get us one. The second argument is important: it passes the authority of a FileProvider. It must be equal to the
authority of the FileProvider defined in the manifest.
GoalKicker.com – Android™ Notes for Professionals 382
Share the file with other apps
We use ShareCompat to share the file with other apps:
Intent intent = ShareCompat.IntentBuilder.from(getContext())
.setType("application/pdf")
.setStream(uri)
.setChooserTitle("Choose bar")
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Context.startActivity(intent);
A chooser is a menu from which the user can choose with which app he/she wants to share the file. The flag
Intent.FLAG_GRANT_READ_URI_PERMISSION is needed to grant temporary read access permission to the URI.
GoalKicker.com – Android™ Notes for Professionals 383
Chapter 50: Storing Files in Internal &
External Storage
Parameter Details
name The name of the file to open. NOTE: Cannot contain path separators
mode
Operating mode. Use MODE_PRIVATE for default operation, and MODE_APPEND to append an existing file.
Other modes include MODE_WORLD_READABLE and MODE_WORLD_WRITEABLE, which were both deprecated
in API 17.
dir Directory of the file to create a new file in
path Path to specify the location of the new file
type
Type of files directory to retrieve. Can be null, or any of the following: DIRECTORY_MUSIC,
DIRECTORY_PODCASTS, DIRECTORY_RINGTONES, DIRECTORY_ALARMS, DIRECTORY_NOTIFICATIONS,
DIRECTORY_PICTURES, or DIRECTORY_MOVIES
Section 50.1: Android: Internal and External Storage -
Terminology Clarification
Android developers(mainly beginners) have been confused regarding Internal & External storage terminology.
There are lot of questions on Stackoverflow regarding the same. This is mainly because of the fact that terminology
according to Google/official Android documentation is quite different to that of normal Android OS user. Hence I
thought documenting this would help.
What we think - User’s Terminology (UT)
Internal storage(UT) External storage(UT)
phone’s inbuilt internal memory removable Secure Digital(SD) card or micro SD storage
Example: Nexus 6P's 32 GB internal
memory.
Example: storage space in removable SD cards provided by vendors
like samsung, sandisk, strontium, transcend and others
But, According to Android Documentation/Guide - Google’s Terminology (GT)
Internal storage(GT):
By default, files saved to the internal storage are private to your application and other applications cannot
access them (nor can the user).
External storage(GT):
This can be a removable storage media (such as an SD card) or an internal (non-removable) storage.
External Storage(GT) can be categorized into two types:
Primary External Storage Secondary External Storage or Removable storage(GT)
This is same as phone’s inbuilt internal memory
(or) Internal storage(UT)
This is same as removable micro SD card storage (or) External
storage(UT)
Example: Nexus 6P's 32 GB internal memory. Example: storage space in removable SD cards provided by
vendors like samsung, sandisk, strontium, transcend and others
GoalKicker.com – Android™ Notes for Professionals 384
This type of storage can be accessed on windows
PC by connecting your phone to PC via USB cable
and selecting Camera(PTP) in the USB options
notification.
This type of storage can be accessed on windows PC by
connecting your phone to PC via USB cable and selecting File
transfer in the USB options notification.
In a nutshell,
External Storage(GT) = Internal Storage(UT) and External Storage(UT)
Removable Storage(GT) = External Storage(UT)
Internal Storage(GT) doesn't have a term in UT.
Let me explain clearly,
Internal Storage(GT): By default, files saved to the internal storage are private to your application and other
applications cannot access them. Your app user also can't access them using file manager; even after enabling
"show hidden files" option in file manager. To access files in Internal Storage(GT), you have to root your Android
phone. Moreover, when the user uninstalls your application, these files are removed/deleted.
So Internal Storage(GT) is NOT what we think as Nexus 6P's 32/64 GB internal memory
Generally, Internal Storage(GT) location would be:
/data/data/your.application.package.appname/someDirectory/
External Storage(GT):
Every Android-compatible device supports a shared "external storage" that you can use to save files. Files
saved to the external storage are world-readable and can be modified by the user when they enable USB
mass storage to transfer files on a computer.
External Storage(GT) location: It could be anywhere in your internal storage(UT) or in your removable storage(GT)
i.e. micro SD card. It depends on your phone's OEM and also on Android OS version.
In order to read or write files on the External Storage(GT), your app must acquire the READ_EXTERNAL_STORAGE or
WRITE_EXTERNAL_STORAGE system permissions.
For example:
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
If you need to both read and write files, then you need to request only the WRITE_EXTERNAL_STORAGE
permission, because it implicitly requires read access as well.
In External Storage(GT), you may also save files that are app-private
But,
When the user uninstalls your application, this directory and all its contents are deleted.
GoalKicker.com – Android™ Notes for Professionals 385
When do you need to save files that are app-private in External Storage(GT)?
If you are handling files that are not intended for other apps to use (such as graphic textures or sound
effects used by only your app), you should use a private storage directory on the external storage
Beginning with Android 4.4, reading or writing files in your app's private directories does not require the
READ_EXTERNAL_STORAGE or WRITE_EXTERNAL_STORAGE permissions. So you can declare the permission
should be requested only on the lower versions of Android by adding the maxSdkVersion attribute:
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18" />
...
</manifest
Methods to store in Internal Storage(GT):
Both these methods are present in Context class
File getDir (String name, int mode)
File getFilesDir ()
Methods to store in Primary External Storage i.e. Internal Storage(UT):
File getExternalStorageDirectory ()
File getExternalFilesDir (String type)
File getExternalStoragePublicDirectory (String type)
In the beginning, everyone used Environment.getExternalStorageDirectory() , which pointed to the root of Primary
External Storage. As a result, Primary External Storage was filled with random content.
Later, these two methods were added:
1. In Context class, they added getExternalFilesDir(), pointing to an app-specific directory on

Comments

Popular posts from this blog

Android 4?

Android3?