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:
- Build high quality, robust apps
- Eliminate boilerplate code
- Accelerate development
- Architecture components
- Foundation components
- AppCompat
- Android KTX
- Multidex
- Test
- Behaviour Components
- Download manager
- Media & playback
- Notifications
- Permissions
- Sharing
- Slices
- UI components
- Animation & transitions
- Auto
- Emoji
- Fragment
- Layout
- Palette
- TV
- Wear OS by Google
-
Room
- Fluent SQLite database access using Object Mapping Technique.
- Need:
- Less Boiler Plate code .i.e., Sends data to the SQLite database as Objects and not as Content Values and returns the result of executing a query as Objects as Cursors.
- Compile time validation of SQLite Queries.
- Support for Observations like Live Data and RxJava
- Makes use of Annotations for it's functioning and makes the API simple and neat to use.
- Annotations:
- @Entity: Defines the schema of the database table.
- @dao: Stands for Database Access Object, used to perform read or write operations on the database
- @database: As a database Holder. Creates a new Database based on supplied conditions, if it already exists it establishes a connection to the pre-existing database.
- Code:
- @Entity:
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.
@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;} }
- Room should have access to the member varaibles of the Entity class .i.e., it should be able to set or reset or update the value stored in a member variable and hence note that though the member variable is declared as private there is a public constructor and a getter to access the member variable.
- @dao:
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 and no a Cursor.
@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(); }
- If you replace
List<Word>
byLiveData<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:
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
@Database(entities = {Word.class}, version = 1) public abstract class WordRoomDatabase extends RoomDatabase { public abstract WordDao wordDao(); }
- Database Connection:
Here,
WordRoomDatabase db = Room.databaseBuilder(context.getApplicationContext(), WordRoomDatabase.class, "word_database") .fallbackToDistructiveMigration() .build();
.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. - Additional Hints
- Use
@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. - To pass a value of a member variable into the query make use of
:
.i.e.,@Query("SELECT * from word_table ORDER BY word LIKE :current_word")
- Create and Make use of a Repository class to handle the Data Operations. You can handle custom logics such as if you're fetching data from the network but there is no existing connection to the internet, then you'll have to fetch and display the cached data or from dao.
- Use
- @Entity:
-
View Model
- Manage UI-related data in a lifecycle-conscious way.
- Since it survives configuration changes it has the potential to replace AsyncTask Loaders.
- Need:
- High decoupling of UI and Data.
- No gaint activities with too many responsibilities.
- UI Controller displays data.
- ViewModel holds the UI Data.
- Reason:
- Say for an instance you're having an Activity that has a number of UI elements and that will have it's state updated based on the user inputs and now when the device undergoes configuration change the data will be lost since the activity has been recreated due to the configuration change. Of course, you can make use of
onSavedInstanceState()
but having forgotten a single field shall lead to inconsistency in the app, here is where ViewModel comes in to play things smart. - Here in ViewModel since the UI data is decoupled from the activity lifecycle it can out-live the impact made by configuration changes on the data. Therefore, after recreation of the activity the UI Data can be fetched from the ViewModel to update the UI respectively.
- Representation:
- Code:
- Extend from
ViewModel
orAndroidViewModel
public class MyViewModel extends ViewModel { // Should contain all the UI Data (As LiveData if possible) // Getters and Setters }
- Linking
ViewModel
to the Activitypublic 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 }); } }
- Important Note:
View Model
can survive:- Configuration Changes
View Model
doesn't survive:- Pressing Back .i.e., Destroying the activity.
- Killing the Activity through Multi-tasking.
View Model
is not a replacement for:- Presistence
- onSavedInstanceState
- Linked to the
Activity lifecycle
and not with theApp lifecycle
.
- Extend from
-
Live Data:
- Notify views when underlying database changes
- Need:
- Reactive UI.
- Updates only on reaching
onStart()
andonResume()
. - Cleans up itself automatically.
- Allows communication with Database to the UI without knowing about it.
- Representation:
- Code:
- LiveData can be made possible for
String
usingMutableLiveData<String>
- To update the UI with real-time data from Live-Data we can make use of a pattern similar to the Observer Pattern.
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); } });
- Here,
mModel
is the ViewModel instance and assumegetCurrentName()
returns aMutableLiveData<String>
and the observer is set over it which invocatesonChanged()
when the data in the MutableLiveData changes. - Arguments:
this
: Specifies the activity that it has the work on .i.e., LifeCycleOwnerObserver<type>
: type depends on the return type of the Live Data.
- LiveData can be made possible for
-
Life Cycle
- Terms:
- Lifecycle: An Object that defines Android Lifecycle.
- Lifecycle Owner: It's an interface:
- Objects with Lifecycles
- eg. Activities and Fragments.
- LifecycleObserver: Observes LifecycleOwner.
- Code:
- Let's say we want a Listener that has to be Lifecycle aware:
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());
- Now perform action based on the current state of the Activity or Fragment.
public class MyObserver implements LifecycleObserver { public void enable(){ enable = true; if(lifecycle.getState().isAtleast(STARTED)){ // action after onStart is reached } }
- Let's say we want a Listener that has to be Lifecycle aware:
- Important Note:
- Initially Lifecycle Library was kept optional but now it has become a fundamental part of Android development and the AppCompatActivity and Fragment class inherit from the Lifecycle Library out of the box.
class AppCompatActivity : LifecycleOwner class Fragment : LifecycleOwner
- Initially Lifecycle Library was kept optional but now it has become a fundamental part of Android development and the AppCompatActivity and Fragment class inherit from the Lifecycle Library out of the box.
- Terms:
-
Data Binding
- Declaratively bind observable data to UI elements.
- It's a proven solution for boiler plate free UIs.
- Code:
data class User(val name: String, ... )
<layout> <data> <variable name="user" type="User"/> </data> <TextView android:text="@{user.name}"/> </layout>
- Here the code is self-explanator, Consider you're having a
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.
- Here the code is self-explanator, Consider you're having a
- Important Note:
- They have native support for LiveData and hence LiveData can be used similar to the above shown instance.
- But this is not enough to observe it since data binding doesn't have a lifecycle. This can be fixed by adding a LifecycleOwner while creating a binding instance.
binding.setLifecycleOwner(lifecycleOwner)
- This can help keep the data in the UI upto date with the LiveData.
-
Paging
- Gradually load information on demand from your data source.
- In other words, Lazy loading for Recycler View.
- Need
- Convenience
- Multiple Layer
- Performance
- Lifecycle aware
- Flexible with different background API and Data Structures.
- If there is a very large list of data to be loaded it's very in-efficient in handling memory constraints also the source of data can be from different locations like some from cloud and others from database if the network in not available and so on.
- Paging library comes with a
PagedList
class which is a Java List implementation that works with a data source Asynchronously. - Every time you access an item in the PagedList it pulls data from the data source Lazily .i.e., on scrolling brings in more data from the data source.
- One of the most common problems is that, if the data is fetched from data source that has thousands of rows returned as a result of query these thousand words are kept ready to inflate it in the UI even though the user can see only 8 - 10 rows of the query result. This is in- efficient(Note that the recycler view can only help in handling the resorces by recycling the list item resources and it has nothing to do with the data). Here is where the Paging stands up by providing data on-demand (when the user scrolls) to the recycler view.
- Code:
- Works great if
Room
is used. - Paging is a bit compicated to explain with simple code samples since it involves a recycler view. But we would highly recommend you going through the Codelab for paging and checkout the paging Sample app so that you get a much more clear picture of it.
- It is highly similar to creating a recycler view and involves some changes in the Adapter (as PageListAdapter) and LiveData.
- Works great if
- Important Note:
- Paging supports RxJava.
- ListAdapter
ListAdapter is a RecyclerView Adapter thet displays a list.
// 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.
- Allows Place Holders
- Without using Place holders: Considers the list size as the size of the list until it's paged and once we've reached the bottom of the paged items the scroll bar jumps to the middle of the scroll interface and we can scroll through the next set of list items that are paged after reaching the end of the firstly paged list. This is not so good in principle and even worse when it comes to handling animation of the scroll bar.
- With Place Holders:
The size of the entire list is taken until the item data becomes null then the contents that are paged are displayed in the list and remaning are shown in the form of place holders which are enabled by default, once they are paged the data will fill up those place holders.
- Depends on the Data Source like
- Positional Data Source (best option)
- Jump to the middle of the list using scroll then start paging from the initial position visible .i.e., visible placeholder.
- ItemKeyed Data Source
- Given a page, the previous page can be identified using first item in the list and the following item can be identified using the last item in the list. If the data source is updated in the mean-while fetch the page sized list from the beginning element of the visible list and perform diff and inflate the changes.
- PageKeyed Data Source
- The best way:
- Positional Data Source (best option)
-
Navigation
- Handle everything needed for in-app navigation.
- Need:
- Animations.
- Argument Passing with type safety.
- Up and Back button press behaviour.
- DeepLink handling.
- No more Fragment transactions.
- It's a visual component added in Android Studio which helps user define flow in their app from one activity to an other or from one fragment to an other.
- Say for an instance if a user enters into your app using deep links and on pressing back/up button the user should be taken to the previous screen in the app and should not exit the app. This hierarchy can be easily defined using navigation.
- The preceeding activities/fragments are pushed into the
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. - Representation:
- The control flow is shown by using pointed arrows to the succeeding activities.
-
Work Manager
- Manage your Android background jobs.
- Need
- Solution for Deferrable Garanteed Execution.
- This means when any of the following tasks happen and if the user is not connected to the network then the tasks should be reserved and once the network connection is restored the appropriate task has to be done.
- Tasks
- Sending a tweet
- Uploading logs
- Periodic data sync
- Solution for Deferrable Garanteed Execution.
- This is a successor (kinda) of JobScheduler and Firebase Job Dispacher since the problem with the latters is that, it's hard to implement and has different behaviours and different APIs.
- Concepts:
- Workers which executes the actions.
- Work Managers which trigger the workers.
- Code:
// Extend from the worker class class CompressWorker : Worker() { //implement the doWork() override fun doWork(): Result { return Result.SUCCESS } }
- Set Constraints:
val myConstraints = Constraints.Builder() .setRequiresDeviceIdle(true) .setRequiresCharging(true) .build() val compressionWork = OneTimeWorkRequestBuilder<CompressWorker>() .setConstraints(myConstraints) .build()
- Additionals:
.setBackoffCriteria
mention what to do if the task is failing.- Pass in arguments like:
.setInputData( mapOf("sample" to "sample1").toWorkData() )
- Invocation:
- Get an instance of the WorkManager and enqueue the work
WorkManager.getInstance().enqueue(work)
- Get an instance of the WorkManager and enqueue the work
- Set Constraints:
- Input and Output semantics
- Not only takes Input but also provide Output data. This can be accomplished by observing the data through WorkManager
- Architecture
- Special Case
- Consider a senario in which we have an image that has to be processed and uploaded to the server. Here we have 2 tasks, where task 1 .i.e., Image Processing can be done without a network and task 2 .i.e., Uploading to server requires a network. This can be easily accomplished using Work Manager.
// 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()
- Consider a senario in which we have an image that has to be processed and uploaded to the server. Here we have 2 tasks, where task 1 .i.e., Image Processing can be done without a network and task 2 .i.e., Uploading to server requires a network. This can be easily accomplished using Work Manager.
- Important Note
- Similar to the above scenario not just 2 but a number of work can be enqueued in the WorkManager by providing proper constraints.
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()
- Opportunistic Execution is ensured. Consider you're sending an email this job is scheduled to JobScheduler or Firebase JobDispacher but the problem is we're not sure of how much time it would take to complete the task and we have no control over it and hence results in a bad user experience. To work arround this we'll have a Thread Pool and run the same thing there as well and we take care of replicating when the JobScheduler calls us back. This can be completely taken care by Work Manager now.
- Similar to the above scenario not just 2 but a number of work can be enqueued in the WorkManager by providing proper constraints.
-
Notifications
-
A guildline for creating a unified notification system handling a wide variety of situations
-
Need
- Notification Anatomy and the most common parts of
appName()
setContentTitle()
setContentText()
setSmallIcon()
andsetLargeIcon()
which handle icon sizesetWhen()
andsetShowWhen(False)
to show timestamps
- Notification Badges and Notification Actions which expand showing more information
- Lockscreen Notifications and Ordering using
setCategory()
setPriority()
- Notification Channels/Categories support which allows for grouping of notifications to specific scenarios
- Notification Anatomy and the most common parts of
-
Representation:
-
Code:
-
Notification Anatomy
- Requires the support library
dependencies { implementation "com.android.support:support-compat:28.0.0" }
- Setting Notification Content
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)
- Add action buttons to the notification
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)
- Setting notification's tap action
// 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
- Enabling badges
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)
- Setting custom notification counts
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()
- Modifying a notifications long-press menu icon
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
- Setting visibility
notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); //visibility is handled where VISBILITY_SECRET is mentioned notificationManager.createNotificationChannel(notificationChannel);
- There is 3 types of visibility practices:
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.
- Setting the categories
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
- Channel Creation
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) } }
- Setting importance level has 4 options:
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 bar
-
-
-
Permissions
-
Need
- Android apps require permissions from the user to access sensitive Data from the device.
- Normal permissions such as
ACCESS_NETWORK_STATE
ACCESS_WIFI_STATE
SET_WALLPAPER
VIBRATE
WAKE_LOCK
etc are provided by the Android system itself.
- However Dangerous permissions like,
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.
- In Android 5.1.1(Lollipop - API 22) or lower requests all dangerous permissions in the install time itself. If they are granted, only then the app will be installed
- In Android 6.0 (Marshmallow - API 23 or newer) requests the dangerous permissions only at the runtime.
- So your app should always check and request permissions at runtime to prevent security Exceptions and app crashing.
-
Representation:
-
Code:
- In the App Manifest:
Add the required
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>
- In the Activity requiring the permission:
// 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 }
- Handling the user's response:
- After the user grants or denies the permission, the system invoke onRequestPermissionsResult method. Your App must override the method and handle the functionality if the permission is granted, or disable that particular functionality if denied. If the user denies the permission for multiple times the system them also shows the dialog with the
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. } }
- After the user grants or denies the permission, the system invoke onRequestPermissionsResult method. Your App must override the method and handle the functionality if the permission is granted, or disable that particular functionality if denied. If the user denies the permission for multiple times the system them also shows the dialog with the
- Permissions of Optional Hardware:
- For using certain harware features such as camera or GPS your app needs permission. However not all devices posses all the required hardwares. So, in the Android Manifest file request for the permission as shown below,
<uses-feature android:name="android.hardware.camera" android:required="false" />
- Unless the android:required attribute is specified to false, your app will be listed "only" to devices that have the hardware.
- In the App Manifest:
Add the required
-
- Emoji
- EmojiCompat renders Emojis even if they don't exist in old font packages used in an older OS.
- Need
- Backwards compatibility of Emojis with older Android OS (API level 19 or higher)
- Prevents Emojis from being displayed as invalid by creating an Emoji span
- Code
- In the App Manifest: add the following meta-data tag
<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:- Download Emoji set at install time:
// 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);
- Bundle Emoji set with the apk:
(Note that this increases apk size and requires app updates upon each Emoji update)
// initialize EmojiCompat such that retrieves the fonts locally val config = BundledEmojiCompatConfig(this); EmpjiCompat.init(config);
-
Usage:
- Display emojis by using widgets that extend from AppCompat widgets
<android.support.text.emoji.widget.EmojiAppCompatTextView android:layout_width="wrap_content" android:layout_height="wrap_content" />
-
Other features:
- How to check if EmojiCompat is finished initializing:
Inintializing EmojiCompat may take some time so checking if it is complete may be necesasry before proceeding with displaying Emojis
val config = FontRequestEmojiCompatConfig(this, fontRequest) .registerInitCallBack(object : EmojiCompat.InitCallback() { // your code to execute when initializing is complete });
- How to indicate an Emoji span:
(useful for debugging)
// colors all Emoji spans in the chosen color to distinguish them from Emojis rendered normally val config = FontRequestedEmojiCompatConfig(...) .setEmojiSpanIndicatorEnabled(true) .setEmojiSpanIndicatorColor(Color.MAGENTA);
- How to Preprocess a character sequence instead of repatedly using a raw string:
// processes the Emoji normally if in current font package, otherwise as Emoji Span val processed : CharSequence = EmojiCompat.get() .process("neutral face \uD83D\uDE10");
- To add Emoji support to a textView Subclass, use the EmojiTextViewHelper
Class MyCheckBox(context: Context) : AppCompatCheckBox(context) { private val helper: EmojiTextViewHelper = ... init { helper.updateTransformationMethod(); } override fun setAllCaps(allCaps: Boolean) { super.setAllCaps(allCaps) helper.setAllCaps(allCaps); } }
- How to check if EmojiCompat is finished initializing:
-
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
- The UI consists of a hierarchy of objects called views β every element of the screen is a view. The View class represents the basic building block for all UI components, and the base class for classes that provide interactive UI components such as buttons, checkboxes, and text entry fields.
-
ViewGroup
- Views can be grouped together inside a view group, which acts as a container of views. The relationship is parent-child, in which the parent is a view group, and the child is a view or view group within the group.
-
Layout Attributes
- ID
- These ids are typically assigned in the layout XML files, and are used to find specific views within the view tree.View IDs need not be unique throughout the tree.
- These ids are typically assigned in the layout XML files, and are used to find specific views within the view tree.View IDs need not be unique throughout the tree.
- Height & Width
- It describes about the Height and Width of the view.
- Before Moving into Margin and Padding See the below Image to understand the basic view of Margin and Padding.
- It describes about the Height and Width of the view.
- Margin and Padding
- Margins are the spaces outside the border, between the border and the other elements next to this view.
- Padding is the space inside the border, between the border and the actual viewβs content.
- Gravity & Layout_Gravity
- Gravity specifies how child Views are positioned.
- Layout_Gravity specifies how much of the extra space in the layout should be allocated to the View.
- ID
-
Types
- Linear Layout
- Relative Layout
- Frameout Layout
- Constraint Layout
- Linear Layout
-
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
- In a relative layout every element arranges itself relative to other elements or a parent element.
-
Frame Layout
- Frame Layout is designed to block out an area on the screen to display a single item. Generally, FrameLayout should be used to hold a single child view, because it can be difficult to organize child views in a way that's scalable to different screen sizes without the children overlapping each other.
- You can, however, add multiple children to a FrameLayout and control their position within the FrameLayout by assigning gravity to each child, using the android:layout_gravity attribute.
- Child views are drawn in a stack, with the most recently added child on top.
-
Constraint Layout
- A ConstraintLayout is a ViewGroup which allows you to position and size widgets in a flexible way.
- Note
- ConstraintLayout is available as a support library that you can use on Android systems starting with API level 9 (Gingerbread).
- There are currently various types of constraints that you can use:
- Relative positioning
- Margins
- Centering positioning
- Circular positioning
- Visibility behavior
- Dimension constraints
- Chains
- Virtual Helpers objects
- Optimizer
- A ConstraintLayout is a ViewGroup which allows you to position and size widgets in a flexible way.
-
-
Palette API
- Need:
- Extract useful color information form image.
- Classify and obtain the color sense of an image.
- Playing with colors to make the UI render based on the Dominant color of the image.
- Obtain color information for the images obtained even through network calls.
- Extract useful color information form image.
- Code
- Add the Dependency:
implementation 'com.android.support:palette-v7:28.0.0'
- Crate a Palette Instance:
// 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 } }
- Note:
- Use synchronous palette generation if you want to create the palette on the same thread as the method being called else do it asynchronously.
- The input should be given in the form of
bitmap
so consider converting the image into a bitmap.
- Add the Dependency:
- Possible Color Profiles:
- Light Vibrant
- Vibrant
- Dark Vibrant
- Light Muted
- Muted
- Dark Muted
- Example
- Following code snippet gets the color information from the image obtained through Glide and sets the backgroud to the color profile of the image obtained through Palette API.
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)) } } } })
- Need:
-
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:
- Color palettes (Darker palettes allow for better battery life for OLEDs)
- Adopt vertical layouts
- Horizontal swipe (Reserved for activity dismissal)
- Make use of user interface components
-
Code:
- Generating a palette instance:
// 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 } }); } }
<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>
- Horizontal Swipe for Dismissing Activities:
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
- Complications are features of a watch face that are displayed in addition to the time. The Complications API allows for seamless integration with API calls.
- Need:
- Exposing data to complications
- Adding complications to a watch face
- Code:
- Exposing data to 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); } }
- Discussion:
To respond to update requests from the system, your data provider app must implement the onComplicationUpdate() method of theComplicationProviderService
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
- Setting other data providers
startActivityForResult( ComplicationHelperActivity.createProviderChooserHelperIntent( getActivity(), watchFace, complicationId, ComplicationData.TYPE_LARGE_IMAGE),PROVIDER_CHOOSER_REQUEST_CODE);
- Representation:
- Discussion:
Watch faces can call the createProviderChooserHelperIntent method to obtain an intent that can be used to show the chooser interface. When the user selects a data provider, the configuration is saved automatically; nothing more is required from the watch face.
- Setting other data providers
-
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); } ```
- Discussion:
A watch face calls setActiveComplications(), in the WatchFaceService.Engine class, with a list of watch face complication IDs. A watch face creates these IDs to uniquely identify slots on the watch face where complications can appear. Complication data is delivered via the onComplicationDataUpdate() callback.
- Stand Alone Functionality
- The use of a Wear OS application to communicate with the cloud without the requirement of a corresponding bridge application on your Android smartphone. Wear OS also has the Google Play store in order to download applications straight to a Wear OS device
- Need:
- Standalone Identifier
- Code:
- Standalone Identifier
<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>
- Standalone Identifier
- Discussion:
Since a standalone app (that is, an independent or semi-independent app) can be installed by an iPhone user or a user of an Android phone that lacks the Play Store, the watch app should be usable without the phone app.
-
- 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!