Skip to content

πŸš€ Road to Accelerate Android Development using Jetpack

License

Notifications You must be signed in to change notification settings

Fenscode/android-jetpack

Β 
Β 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

96 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Image

Ultimate Android Jetpack Reference

Android ARCore In Progress

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

Show Some ❀️ by

GitHub stars GitHub followers Twitter Follow

Categories

Architecture Components

  • 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:
              @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.
      • 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:
            @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 and no a Cursor.
      • If you replace 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:
            @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
      • Database Connection:
            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.
      • 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.
  • 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 or AndroidViewModel
            public class MyViewModel extends ViewModel {
                // Should contain all the UI Data (As LiveData if possible)
                // Getters and Setters
            }
      • Linking 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
                    });
                }
            }
      • 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 the App lifecycle.
  • Live Data:

    • Notify views when underlying database changes
    • Need:
      • Reactive UI.
      • Updates only on reaching onStart() and onResume().
      • Cleans up itself automatically.
      • Allows communication with Database to the UI without knowing about it.
    • Representation:

    • Code:
      • LiveData can be made possible for String using MutableLiveData<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 assume getCurrentName() returns a MutableLiveData<String> and the observer is set over it which invocates onChanged() when the data in the MutableLiveData changes.
      • Arguments:
        • 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

    • 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
                    }
            }
    • 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
  • 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.
    • 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.
    • 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
            • Used in server-side APIs.
            • Representation:

          • The best way:
            • Representation:

              I

              II

            • The first technique is not so efficient because of the following reasons:
              • Poor network.
              • Not using local data form the Database though they are present.
            • This is over come by the second technique which is self explanatory.
  • 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
    • 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)
    • 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()
    • 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.

Behaviour Components

  • 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() and setLargeIcon() which handle icon size
        • setWhen() and setShowWhen(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
    • Representation:

      • Notification Anatomy

      • Notification Badges and Notification Actions

      • Lockscreen Notifications and Ordering

      • Notification Channels/Categories

    • 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 notification
          • IMPORTANCE_DEFAULT for high importance, makes a sound
          • IMPORTANCE_LOW for medium importance which makes no sound
          • IMPORTANCE_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:

      • Runtime Permissions

      • Install Time permissions

    • 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.
                }
            }
      • 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.

UI Components

  • 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);
                }
            }
  • 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.
      • 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.


      • 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.
    • Types

      • Linear Layout
      • Relative Layout
      • Frameout Layout
      • Constraint 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

  • 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.
    • 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.
    • 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))
                      }
                  }
              }
          })
  • 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  }  });  
      				} 
      			   
      			}
      • Representation:
        • Wear OS Palette List

        • Corresponding Wear OS UI

      • Vertical Layout
      <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:

      • Do and Don't:

      • Using both vertical and horizontal scrolling can make traversing apps confusing, Stick to vertical.
    • 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 the 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

      • Setting other data providers
                startActivityForResult(
                ComplicationHelperActivity.createProviderChooserHelperIntent(
                getActivity(),
                watchFace,
                complicationId,
                ComplicationData.TYPE_LARGE_IMAGE),PROVIDER_CHOOSER_REQUEST_CODE);
      • Representation:
        • Complications on a Watchface:

      • 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.
    • 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>
      • 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.

TODO

- Complete the remaining components : work in Progress.!

License

   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.

Contributions

Just make pull request. You are in!

Releases

No releases published

Packages

No packages published

Languages

  • Kotlin 100.0%