🚀 Road to Accelerate Android Development using Jetpack
:exclamation: No longer Maintained!
What's Android Jetpack
Android Jetpack is the next generation of components and tools along with Architectural guidance designed to accelerate Android Development. The main reasons are:
Room
@Entity(tableName = "word_table")
public class Word {
@PrimaryKey
@NonNull
@ColumnInfo(name = "word")
private String mWord;
public Word(String word) {this.mWord = word;}
public String getWord(){return this.mWord;}
}
Most of the annotations are self-explanatory where the @Entity takes in and declares the tableName of the table that's about to be created (if tableName is not mentioned the table takes up the name of the class, here in this case : Word) and the class also has a number of optional attributes like setting the column name(if node specified it takes up the name of the varibale), It's important to note that the all the declarations inside the entity class are considered as columns of the database.(Here "one" .i.e., mWord). You can make use of @Ignore annotation to ignore a declaration as a column in your database. @Dao
public interface WordDao {
@Insert
void insert(Word word);
@Query("DELETE FROM word_table")
void deleteAll();
@Query("SELECT * from word_table ORDER BY word ASC")
List<Word> getAllWords();
}
Note that Dao should be an abstract class or an interface. The remaining are self explanatory and as said earlier notice that the querying out from the database returns a list of objects in this case .i.e., List<Word> and no a Cursor.List<Word>
by LiveData<List<Word>>
then it happens to return an updated list of objects for every change in the database. This is one of the prime principles of LiveData
@Database(entities = {Word.class}, version = 1)
public abstract class WordRoomDatabase extends RoomDatabase {
public abstract WordDao wordDao();
}
Note that this has to be abstract and extend the class RoomDatabase. Entities contain the database tables that has to be created inside the database and there should be an abstract getter method for @doa
WordRoomDatabase db = Room.databaseBuilder(context.getApplicationContext(),
WordRoomDatabase.class, "word_database")
.fallbackToDistructiveMigration()
.build();
Here, .fallbackToDistructiveMigration()
is an optional attribute that signifies if the database schema is changed wipe out all the data existing in the currently existing database.
Creating multiple instances of the database is expensive and is not recommended and hence keeping it as a singleton(which can have only one instance of the class) can come in handy.@PrimayKey(autoGenerate = true)
to auto generate the primary key, it's usually applied to Integer values and hence starts from 1 and keeps incrementing itself.:
.i.e.,
@Query("SELECT * from word_table ORDER BY word LIKE :current_word")
View Model
onSavedInstanceState()
but having forgotten a single field shall lead to inconsistency in the app, here is where ViewModel comes in to play things smart.
ViewModel
or AndroidViewModel
public class MyViewModel extends ViewModel {
// Should contain all the UI Data (As LiveData if possible)
// Getters and Setters
}
ViewModel
to the Activity
public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same MyViewModel instance created by the first activity.
// ViewModelProvider provides ViewModel for a given lifecycle scope.
MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
model.getUsers().observe(this, users -> {
// update UI
});
}
}
View Model
can survive:
View Model
doesn't survive:
View Model
is not a replacement for:
Activity lifecycle
and not with the App lifecycle
.Live Data:
onStart()
and onResume()
.
String
using MutableLiveData<String>
mModel.getCurrentName().observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable final String newName) {
// Update the UI, in this case, a TextView.
mTextView.setText(newName);
}
});
mModel
is the ViewModel instance and assume getCurrentName()
returns a MutableLiveData<String>
and the observer is set over it which invocates onChanged()
when the data in the MutableLiveData changes.this
: Specifies the activity that it has the work on .i.e., LifeCycleOwner
Observer<type>
: type depends on the return type of the Live Data.Life Cycle
public class MyObserver implements LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void connectListener() {
...
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void disconnectListener() {
...
}
}
lifecycleOwner.getLifecycle().addObserver(new MyObserver());
public class MyObserver implements LifecycleObserver {
public void enable(){
enable = true;
if(lifecycle.getState().isAtleast(STARTED)){
// action after onStart is reached
}
}
class AppCompatActivity : LifecycleOwner
class Fragment : LifecycleOwner
Data Binding
data class User(val name: String, ... )
<layout>
<data>
<variable name="user" type="User"/>
</data>
<TextView android:text="@{user.name}"/>
</layout>
data class
in Kotlin that has a number of fields that has the real-time data that has to be updated in the UI then it can be directly parsed in the XML document of the UI by creating an appropriate variable for accessing the data class as shown above. binding.setLifecycleOwner(lifecycleOwner)
Paging
PagedList
class which is a Java List implementation that works with a data source Asynchronously.Room
is used. // Call 1
listAdapter.submitList(listOf(1,2,3,4))
// Call 2
listAdapter.submitList(listOf(1,3,4))
// when second statement is executed after executing 1st it simply removes '2' because it finds the difference between the two updates in the background thread and perform UI update correspondingly. This makes it efficient.
I
II
Navigation
back stack
as the deep link is clicked and going back shall lead to popping of contents from the back stack to reach the previous screens.
Work Manager
// Extend from the worker class
class CompressWorker : Worker() {
//implement the doWork()
override fun doWork(): Result {
return Result.SUCCESS
}
}
val myConstraints = Constraints.Builder()
.setRequiresDeviceIdle(true)
.setRequiresCharging(true)
.build()
val compressionWork = OneTimeWorkRequestBuilder<CompressWorker>()
.setConstraints(myConstraints)
.build()
.setBackoffCriteria
mention what to do if the task is failing. .setInputData(
mapOf("sample" to "sample1").toWorkData()
)
WorkManager.getInstance().enqueue(work)
// Create 2 classes for the 2 tasks
class ImageProcessing : Worker()
class Upload : Worker()
fun createImageWorker(image : File) : OneTimeWorkRequest{
...
}
val processImageWork = createImageWorker(file)
//constraint to upload only in the presence of Network
val constraint = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val uploadImageWork = OneTimeWorkRequestBuilder<UploadImageWorker>()
.setConstraints(constraints)
.build
// Starts with image processing and then initiates upload
WorkManager.getInstance()
.beginWith(processImageWork)
.then(UploadImageWork)
.enqueue()
WorkManager.getInstance()
// First, run all the A tasks (in parallel):
.beginWith(workA1, workA2, workA3)
// ...when all A tasks are finished, run the single B task:
.then(workB)
// ...then run the C tasks (in any order):
.then(workC1, workC2)
.enqueue()
Notifications
A guildline for creating a unified notification system handling a wide variety of situations
Need
appName()
setContentTitle()
setContentText()
setSmallIcon()
and setLargeIcon()
which handle icon sizesetWhen()
and setShowWhen(False)
to show timestampssetCategory()
setPriority()
Representation:
Code:
Notification Anatomy
dependencies {
implementation "com.android.support:support-compat:28.0.0"
}
var mBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle(textTitle)
.setContentText(textContent)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
Creating the content requires that you include a small or large icon to be drawn, the content title, the content text holding the information to be shown and the priority. This allows the system to show notifications by priority.
Setting Notifications to be longer than the one line trucation
var mBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle("My notification")
.setContentText("Much longer text that cannot fit one line...")
.setStyle(NotificationCompat.BigTextStyle()
.bigText("Much longer text that cannot fit one line..."))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
val snoozeIntent = Intent(this, MyBroadcastReceiver::class.java).apply {
action = ACTION_SNOOZE
putExtra(EXTRA_NOTIFICATION_ID, 0)
}
val snoozePendingIntent: PendingIntent =
PendingIntent.getBroadcast(this, 0, snoozeIntent, 0)
val mBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle("My notification")
.setContentText("Hello World!")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.addAction(R.drawable.ic_snooze, getString(R.string.snooze),
snoozePendingIntent)
// Create an explicit intent for an Activity in your app
val intent = Intent(this, AlertDetails::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
val mBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle("My notification")
.setContentText("Hello World!")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
// Set the intent that will fire when the user taps the notification
.setContentIntent(pendingIntent)
.setAutoCancel(true)
Notification Badges and Notification Actions
val id = "my_channel_01"
val name = getString(R.string.channel_name)
val descriptionText = getString(R.string.channel_description)
val importance = NotificationManager.IMPORTANCE_LOW
val mChannel = NotificationChannel(id, name, importance).apply {
description = descriptionText
setShowBadge(true)
//Setting this option to false disables notificication badges
}
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(mChannel)
var notification = NotificationCompat.Builder(this@MainActivity, CHANNEL_ID)
.setContentTitle("New Messages")
.setContentText("You've received 3 new messages.")
.setSmallIcon(R.drawable.ic_notify_status)
.setNumber(messageCount)
//Change the message count here
.build()
var notification = NotificationCompat.Builder(this@MainActivity, CHANNEL_ID)
.setContentTitle("New Messages")
.setContentText("You've received 3 new messages.")
.setSmallIcon(R.drawable.ic_notify_status)
.setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL)
//You must include a small badge icon to show in constant
.build()
Lockscreen Notification and Ordering
notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
//visibility is handled where VISBILITY_SECRET is mentioned
notificationManager.createNotificationChannel(notificationChannel);
VISIBILITY_PUBLIC
shows the notification's full content.VISIBILITY_SECRET
doesn't show any part of this notification on the lock screen.VISIBILITY_PRIVATE
shows basic information, such as the notification's icon and the content title, but hides the notification's full content.var mBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle("My notification")
.setContentText("Hello World!")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
//setting the category here to handle different scenarios
Options include, but not limited to:
CATEGORY_ALARM
CATEGORY_REMINDER
CATEGORY_EVENT
CATEGORY_CALL
Setting the priority
var mBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle(textTitle)
.setContentText(textContent)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
//setting the priority is in the same area where you defined notification anatomy, so the same options are in effect
Notification Channels/Categories
private fun createNotificationChannel() {
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = getString(R.string.channel_name)
val descriptionText = getString(R.string.channel_description)
val importance = NotificationManager.IMPORTANCE_DEFAULT
//importance is handled where IMPORTANCE_DEFAULT is mentioned
val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
description = descriptionText
}
// Register the channel with the system
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
IMPORTANCE_HIGH
for Urgent importance, makes a sound and appears as a heads-up notificationIMPORTANCE_DEFAULT
for high importance, makes a soundIMPORTANCE_LOW
for medium importance which makes no soundIMPORTANCE_MIN
for low importance which does not make a sound or appear in the status barPermissions
Need
ACCESS_NETWORK_STATE
ACCESS_WIFI_STATE
SET_WALLPAPER
VIBRATE
WAKE_LOCK
etc
are provided by the Android system itself.READ_CALL_LOG
CAMERA
READ_CONTACTS
RECORD_AUDIO
ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
etc that can affects user's privacy require explicit permissions from the user.Representation:
Code:
uses-permission
tag as a child to the Manifest element
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.SEND_SMS"/>
<!-- other permissions go here -->
<application ...>
...
</application>
</manifest>
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(thisActivity,
Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
// Permission is not granted
// Should we show an explanation why.
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
Manifest.permission.READ_CONTACTS)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
} else {
// No explanation needed; request the permission
ActivityCompat.requestPermissions(thisActivity,
new String[]{Manifest.permission.READ_CONTACTS},
MY_PERMISSIONS_REQUEST_READ_CONTACTS);
// MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
// app-defined int constant. The callback method gets the
// result of the request.
}
} else {
// Permission has already been granted
}
Never Ask Again
checkbox. Hence always check if the permission is available at runtime. @Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted, yay! Do the
// contacts-related task you need to do.
} else {
// permission denied, boo! Disable the
// functionality that depends on this permission.
}
return;
}
// other 'case' lines to check for other
// permissions this app might request.
}
}
<uses-feature android:name="android.hardware.camera" android:required="false" />
<meta-data
android:name="fontProviderRequests"
android:value="Noto Color Emoji Compat"/>
One-time setup
Getting the Emoji package can be done in one of two ways:
// create a request to download the latest fonts package
val fontRequest = FontRequst(
"com.google.android.gms.fonts",
"com.google.android.gms",
"Noto Color Emoji Compat",
R.array.com_google_android_gms_fonts_certs);
// initialize EmojiCompat such that it sends the request
val config = FontRequestEmojiCompatConfig(this, fontRequest);
EmojiCompat.init(config);
// initialize EmojiCompat such that retrieves the fonts locally
val config = BundledEmojiCompatConfig(this);
EmpjiCompat.init(config);
Usage:
<android.support.text.emoji.widget.EmojiAppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
Other features:
val config = FontRequestEmojiCompatConfig(this, fontRequest)
.registerInitCallBack(object : EmojiCompat.InitCallback() {
// your code to execute when initializing is complete
});
// colors all Emoji spans in the chosen color to distinguish them from Emojis rendered normally
val config = FontRequestedEmojiCompatConfig(...)
.setEmojiSpanIndicatorEnabled(true)
.setEmojiSpanIndicatorColor(Color.MAGENTA);
// processes the Emoji normally if in current font package, otherwise as Emoji Span
val processed : CharSequence =
EmojiCompat.get()
.process("neutral face \uD83D\uDE10");
Class MyCheckBox(context: Context) :
AppCompatCheckBox(context) {
private val helper: EmojiTextViewHelper = ...
init {
helper.updateTransformationMethod();
}
override fun setAllCaps(allCaps: Boolean) {
super.setAllCaps(allCaps)
helper.setAllCaps(allCaps);
}
}
Layout
A layout defines the structure for a user interface in your app, such as in an activity. All elements in the layout are built using a hierarchy of View and ViewGroup objects.
View
ViewGroup
Layout Attributes
Types
Linear Layout
A layout that arranges other views either horizontally in a single column or vertically in a single row.
xml <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> </LinearLayout>
Set android:orientation to specify whether child views are displayed in a row or column.
Relative Layout
Frame Layout
Constraint Layout
Palette API
implementation 'com.android.support:palette-v7:28.0.0'
// Generate palette synchronously and return it
fun createPaletteSync(bitmap: Bitmap): Palette = Palette.from(bitmap).generate()
// Generate palette asynchronously and use it on a different
// thread using onGenerated()
fun createPaletteAsync(bitmap: Bitmap) {
Palette.from(bitmap).generate { palette ->
// Use generated instance
}
}
bitmap
so consider converting the image into a bitmap.
Glide.with(this)
.asBitmap()
.load(imageUrl)
.into(object : SimpleTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
myZoomageView.setImageBitmap(resource)
Palette.from(resource).generate { palette ->
val vibrantSwatch = palette?.mutedSwatch
with(findViewById<ImageView>(R.id.myZoomageView)) {
setBackgroundColor(vibrantSwatch?.rgb ?:
ContextCompat.getColor(context, R.color.colorPrimary))
}
}
}
})
Wear OS
Now, follows Material Design Guidelines.
A design language with a set of rules, guidelines, components and best practices for creating websites and applications.
Need:
Code:
// Generate palette synchronously and return it
public Palette createPaletteSync(Bitmap bitmap) {
Palette p = Palette.from(bitmap).generate();
return p;
// Generate palette asynchronously and use it on a different
// thread using onGenerated()
public void createPaletteAsync(Bitmap bitmap) {
Palette.from(bitmap).generate(new PaletteAsyncListener() {
public void onGenerated(Palette p) {
// Use generated instance } });
}
}
Wear OS Palette List
Corresponding Wear OS UI
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
// Retrieves the tools
xmlns:tools="http://schemas.android.com/tools"
// Sets default height and width to depend on a parent
android:layout_width="match_parent"
android:layout_height="match_parent"
// Sets default orientation to vertical
android:orientation="vertical">
// Sets text for app face
<TextView android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_square" />
</LinearLayout>
public class SwipeDismissFragment extends Fragment {
private final Callback mCallback =
new Callback() {
@Override
public void onSwipeStart() {
// optional
}
@Override
public void onSwipeCancelled() {
// optional
}
@Override
public void onDismissed(SwipeDismissFrameLayout layout) {
// Code here for custom behavior such as going up the
// back stack and destroying the fragment but staying in the app.
}
};
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
SwipeDismissFrameLayout swipeLayout = new SwipeDismissFrameLayout(getActivity());
// If the fragment should fill the screen (optional), then in the layout file,
// in the android.support.wear.widget.SwipeDismissFrameLayout element,
// set the android:layout_width and android:layout_height attributes
// to "match_parent".
View inflatedView = inflater.inflate(R.layout.swipe_dismiss_frame_layout, swipeLayout, false);
swipeLayout.addView(inflatedView);
swipeLayout.addCallback(mCallback);
return swipeLayout;
}
}
Representation:
Watch Face Complications
@Override
public void onComplicationUpdate(
int complicationId, int dataType, ComplicationManager complicationManager) {
Log.d(TAG, "onComplicationUpdate() id: " + complicationId);
// Used to create a unique key to use with SharedPreferences for this complication.
ComponentName thisProvider = new ComponentName(this, getClass());
// Retrieves your data, in this case, we grab an incrementing number from SharedPrefs.
SharedPreferences preferences =
getSharedPreferences( ComplicationTapBroadcastReceiver.COMPLICATION_PROVIDER_PREFERENCES_FILE_KEY, 0);
int number =
preferences.getInt(
ComplicationTapBroadcastReceiver.getPreferenceKey(
thisProvider, complicationId),
0);
String numberText = String.format(Locale.getDefault(), "%d!", number);
ComplicationData complicationData = null;
switch (dataType) {
case ComplicationData.TYPE_SHORT_TEXT:
complicationData =
new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText(numberText))
.build();
break;
default:
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unexpected complication type " + dataType);
}
}
if (complicationData != null) {
complicationManager.updateComplicationData(complicationId, complicationData);
} else {
// If no data is sent, we still need to inform the ComplicationManager, so
// the update job can finish and the wake lock isn't held any longer.
complicationManager.noUpdateRequired(complicationId);
}
}
ComplicationProviderService
class. This method will be called when the system wants data from your provider - this could be when a complication using your provider becomes active, or when a fixed amount of time has passed.Adding complications to a watch face
startActivityForResult(
ComplicationHelperActivity.createProviderChooserHelperIntent(
getActivity(),
watchFace,
complicationId,
ComplicationData.TYPE_LARGE_IMAGE),PROVIDER_CHOOSER_REQUEST_CODE);
Receiving complication data
private void initializeComplicationsAndBackground() {
...
mActiveComplicationDataSparseArray = new SparseArray<>(COMPLICATION_IDS.length);
// Creates a ComplicationDrawable for each location where the user can render a
// complication on the watch face. In this watch face, we create one for left, right,
// and background, but you could add many more.
ComplicationDrawable leftComplicationDrawable =
new ComplicationDrawable(getApplicationContext());
ComplicationDrawable rightComplicationDrawable =
new ComplicationDrawable(getApplicationContext());
ComplicationDrawable backgroundComplicationDrawable =
new ComplicationDrawable(getApplicationContext());
// Adds new complications to a SparseArray to simplify setting styles and ambient
// properties for all complications, i.e., iterate over them all.
mComplicationDrawableSparseArray = new SparseArray<>(COMPLICATION_IDS.length);
mComplicationDrawableSparseArray.put(LEFT_COMPLICATION_ID, leftComplicationDrawable);
mComplicationDrawableSparseArray.put(RIGHT_COMPLICATION_ID, rightComplicationDrawable);
mComplicationDrawableSparseArray.put(
BACKGROUND_COMPLICATION_ID, backgroundComplicationDrawable);
// Recieves data from complication ids within the array
setComplicationsActiveAndAmbientColors(mWatchHandHighlightColor);
setActiveComplications(COMPLICATION_IDS);
}
```
<application>
...
<meta-data
android:name="com.google.android.wearable.standalone"
// android value of true means the Wear OS application is standalone
// value is false if it is dependant on a phone application
android:value="true" />
...
</application>
- Complete the remaining components : work in Progress.!
Copyright 2018 Syam Sundar K
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Just make pull request. You are in!