Jetpack Components
Android Architecture Components
- View Model
- Work Manager
- Live Data
- Room
- Data Binding
- Navigation
- Paging
These help us in creating robust, testable, maintainable and has less boilerplate codeAndroid jetpack is a collection of libraries that makes it easier for us to develop great android application
ViewModel :
- Survives configuration changes
- Not same as onSavedInstanceState() : For small data like a string or integer
- ViewModel for large data bitmap, user list etc.
- Stores and manages the UI related data
- Gets destroyed only when the activity gets destroyed in onCleared()
- Communication layer between UI and DB
- Manages the UI related data in a lifecycle conscious way
- The ViewModel prepares the data which is sent to the UI. So when the
Implementation :
- Extends the class with ViewModel
- Gets destroyed when the activity gets destroyed
- Orientation change has no affect on the data
- Calls onCleared() when it gets destroyed
Live Data :
- Is an observable data holder class and lifecycle aware component
- It keeps the data and allows the data to be observed
- We need to observe the live data from app components onCreate() method so that system doesn't do redundant calls for observing the livedata
- Also it ensures that the activity will have the updated data at this stage
- Its aware if the component that's attached to it is active or not
- Helps us in notify the views when the underlying database data changes
- LiveData
- MutableLiveData
- MediatorLiveData
LiveData is immutable by default. By using LiveData we can only observe the data and cannot set the data.
MutableLiveData is mutable and is a subclass of LiveData. In MutableLiveData we can observe and set the values using postValue() and setValue() methods (the former being thread-safe) so that we can dispatch values to any live or active observers.
MediatorLiveData can observe other LiveData objects such as sources and react to their onChange() events. MediatorLiveData will give us control over when we want to perform an action in particular or when we want to propagate an event.
LiveData will trigger upon configuration change or screen rotation. But in some scenarios, like when we need to perform UI updates by clicking on a particular View to perform validations or show a progress bar during server call, we go with SingleLiveEvent.
SingleLiveEvent is a subclass of MutableLiveData. It is aware of the View's lifecycle and can observe the data with a single Observer
Shared Flow and State Flow
A StateFlow takes a default value through the constructor and emits it immediately while a SharedFlow takes no value and emits nothing by default.
Advantages :
1. No memory leaks cos of lifecycle awareness
Observers are bound to lifecycle objects and cleanup themselves when their associated lifecycle is destroyed
2. Always UI has updated data
if lifecycle becomes inactive, it receives the data upon becoming active again. If the activity is in background, it will receive the update when it comes to foreground.
3. Manages Configuration Changes
If activity is recreated due to orientation changes, it receives the updated data immediately with the help of ViewModel
4. No crash due to stopped activities
If the observers lifecycle is inactive, such as if activity is in back stack, it doesn't receive any LiveData events
5. Ensures the UI matches the data
LiveData notifies the observer object when the lifecycle state changes. Instead of updating the UI every time the app data changes, Observer can change the UI when there is a change
Benefits :
- Automatically gets destroyed when the associated LifeCycleOwner gets destroyed
- Can be shared by multiple resources since its not observed when the activity is stopped
- LiveData is Best when used with ViewModel
Implementation :
Survives configuration changes
Observe the LiveData in onCreate() ensures system doesn't do redundant calls for observing the LiveData
Define the MutableLiveData in the ViewModel
Define the LiveData of the same data type as that of MutableLiveData in the activity to observe the changes in data
eg :
In ViewModel :
private MutableLiveData<String> randomNumber = new MutableLiveData<>();
randomNumber.setValue("Number "+ random.nextInt(10 - 1)+1);
In Activity :
LiveData<String> myNumber = model.getNumber();
myNumber.observe(this, new Observer<String>() {
@Override
public void onChanged(String s) {
tv.setText(s);
Log.i(TAG, "In OnChanged");
}
});
Suppose we are calling an API from 1st activity and we r moving to next activity.
Now the 1st activity is in the stop state and the live data will not be observed but the ViewModel of that activity will have the updated data
So we can conclude that the live data can be observed when the activity that s observing it is in the active or resumed state
Room :
- Is an abstraction layer over SQLite
- Is an ORM(Object Relation Mapping) library
- It easily converts SQLite table data to java objects
- It provides compile time check of SQLite statements and returns the LiveData observable
Components of Room :
Entity :
- Defines the schema of the DB table which has the getter and setter functions
- Transforms the object to a table to store data
- Its annotated by @Entity
- Represents the table in our database
@Entity(tableName = "user_details")
data class User{
@PrimaryKey
@NonNull
var uid : Int
@NonNull
@ColumnInfo(name = "first_name")
var firstName : String?
}
DAO :
- Data Access Objects
- Contains the methods to access database
- Annotated by @DAO
- Its an interface
- For every Entity, a DAO interface is needed that contains functions for accessing the database
Database :
- Database holder class and serves as an main access point to the underlying connection of our apps persistent and relational data
- Annotated with @Database
- The class need to be an abstract class and should extend the RoomDatabase class
- Include the list of entities and mention the version number
- It should have abstract method of each DAO that's related to it
- Create the instance of RoomDatabase using Room.databaseBuilder() and it has to be singleton
Difference between Room and SQLite Database :
- Sqlite deals with raw queries whereas Room doesn't require raw queries
- No Compile time verification. Room has compile time verification of raw SQL queries
- More boilerplate code required to convert from SQL queries to java data objects whereas in Room it maps database objects to java objects with minimal boilerplate code
- SQLite API are low level. So more time and effort is required to build apps whereas Room when used with ViewModel and LiveData makes it easy
- Updating database, changing schema was difficult, Easy in Room with Migration classes
- Cannot work with LiveData and ViewModel, Room is designed to work with LiveData and ViewModel
Singleton class for Room Database
class CustomDatabase private constructor() {
var quoteDao = FakeQuoteDao()
private set
companion object {
@Volatile private var instance : CustomDatabase? = null
// Already instantiated? - return the instance
// Otherwise instantiate in a thread-safe manner
fun getInstance() = instance?: synchronized(this) {
// If it's still not instantiated, finally create an object
// also set the "instance" property to be the currently created one
instance?: CustomDatabase().also { instance = it }
}
}
}
Implementing Room Components in application
- Android application uses DAO to interact with DB
- App uses DAO object to perform DB related operations
- This includes getting entities from DB or persisting the changes back to the DB
- Our application uses the entities to get or set the field value
Inserting data to Room DB :
Fetching the DATA from DB and Displaying it :
Work Manager :
Used to manage the android background jobs
Work Manager is part of Androidx package also known as Jetpack
Its an API that makes it easy to schedule deferrable, asynchronous tasks that are expected to run reliably
Deferrable - Scheduled Mechanism like if it has to run one time, repetitive or Compatible in DozeMode or Power Saving Mode.
Reliably - Run under constraints ie. Run only when the device is connected to Wi-Fi, When the device is idle, When it has sufficient storage space etc,
Always finish the started work - Even if the App exits, Even if the device restarts
WorkParameters - is a mechanism using which we can pass any value to worker class
Helps us to bind the observable data to UI elements
The Data Binding Library is a support library that allows us to bind UI components in our layouts to data sources in our app using a declarative format
Benefits :
- Improves the performance of the app
- Eliminates the use of findViewById() which makes the code concise, easier to read and maintain.
- Recognizes errors during compile time
- Helps us keep the code organized
- Reduces boilerplate code
In build.gradle, under the android tab, add
buildFeatures {
dataBinding true
}
plugins { id 'kotlin-kapt' } annotationProcessor 'androidx.databinding:databinding-compiler:3.2.0-alpha16'
In activity_main.xml,
we need to make <layout> as the parent/root layout which says the android framework that this layout is using Data Binding. A binding class is automatically generated for each layout file. By default the name of the class is based on the name of the layout file. The name of the class we be the name of the layout activity in Pascal Case with Binding added as suffix
Eg: activity_main.xml -->ActivityMainBinding
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="contact"
type="com.keshav.jetpack_udemy.Contact" />
<variable
name="event"
type="com.keshav.jetpack_udemy.EventHandler" />
</data>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="25sp"
android:text="@{contact.name}" />
<TextView
android:id="@+id/tvEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="@{contact.email}" />
<Button
android:id="@+id/btnClick"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="Click Here"
android:onClick="@{() -> event.onButtonClick()}"/>
</LinearLayout>
</LinearLayout>
</layout>
MainActivity.kt
package com.keshav.jetpack_udemy
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import com.keshav.jetpack_udemy.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
// Using Data Binding without defining the <data> and <variable> in XML file
binding.tvName.text = "Keshav Pai"
binding.tvEmail.text = "keshav.pai87@gmail.com"
// Using Data Binding after defining the <data> and <variable> in XML file
binding.contact = Contact("Keshav Pai", "keshav.pai87@gmail.com")
binding.event = EventHandler(this)
}
}
EventHandler.kt
package com.keshav.jetpack_udemy
import android.content.Context
import android.widget.Toast
open class EventHandler(context : Context) {
private val myContext : Context = context
fun onButtonClick() {
Toast.makeText(myContext, "Button Clicked", Toast.LENGTH_SHORT).show()
}
}
Data Class Contact.kt
package com.keshav.jetpack_udemy
data class Contact(var name : String, var email : String)
Working with Observable Data Objects
Observability refers to the capability of an object to notify others about the changes in its data and data binding library allows us to make the objects observable. Data binding library provides the BaseObservable class which implements the listener registration mechanism. When the contact class inherits from BaseObservable class, it will be responsible for notifying when its properties changes. To do that we will use the @Bindable. To make the contact object observable, we will make the class extend BaseObservable.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="contact"
type="com.keshav.jetpack_udemy.Contact" />
<variable
name="event"
type="com.keshav.jetpack_udemy.EventHandler" />
</data>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="25sp"
android:text="@{contact.name}" />
<TextView
android:id="@+id/tvEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="@{contact.email}" />
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/etName"
android:ems="10"
android:inputType="text"
android:text="@={contact.name}"/>
<Button
android:id="@+id/btnClick"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="Click Here"
android:onClick="@{() -> event.onButtonClick(contact.name)}"/>
</LinearLayout>
</LinearLayout>
</layout>
EventHandler.kt
package com.keshav.jetpack_udemy
import android.content.Context
import android.widget.Toast
open class EventHandler(context : Context) {
private val myContext : Context = context
fun onButtonClick(name : String) {
Toast.makeText(myContext, "$name Button Clicked", Toast.LENGTH_SHORT).show()
}
}
Contact.kt
package com.keshav.jetpack_udemy
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
data class Contact(var _name : String, var _email : String) : BaseObservable() {
@get:Bindable
var name : String = _name
set(value) {
field = value
notifyPropertyChanged(BR.name)
}
@get : Bindable
var email : String = _email
set(value) {
field = value
notifyPropertyChanged(BR.email)
}
}
Loading Image from URL using Glide
Glide Dependency
implementation 'com.github.bumptech.glide:glide:4.13.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.13.0'
activtiy_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="contact"
type="com.keshav.jetpack_udemy.Contact" />
<variable
name="event"
type="com.keshav.jetpack_udemy.EventHandler" />
<variable
name="imageUrl"
type="String" />
</data>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp" >
<ImageView
android:id="@+id/ivProfileImage"
android:layout_width="100dp"
android:layout_height="100dp"
android:padding="5dp"
app:profileImage = "@{imageUrl}"
android:src="@mipmap/ic_launcher_round" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="25sp"
android:text="@{contact.name}" />
<TextView
android:id="@+id/tvEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="@{contact.email}" />
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/etName"
android:ems="10"
android:inputType="text"
android:text="@={contact.name}"/>
<Button
android:id="@+id/btnClick"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="Click Here"
android:onClick="@{() -> event.onButtonClick(contact.name)}"/>
</LinearLayout>
</LinearLayout>
</layout>
MainActivity.kt
package com.keshav.jetpack_udemy
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import com.keshav.jetpack_udemy.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
// Using Data Binding without defining the <data> and <variable> in XML file
binding.tvName.text = "Keshav Pai"
binding.tvEmail.text = "keshav.pai87@gmail.com"
// Using Data Binding after defining the <data> and <variable> in XML file
binding.contact = Contact("Keshav Pai", "keshav.pai87@gmail.com")
binding.event = EventHandler(this)
binding.imageUrl = "https://i.redd.it/lhw4vp5yoy121.jpg"
}
}
Contact.kt
package com.keshav.jetpack_udemy
import android.widget.ImageView
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import androidx.databinding.BindingAdapter
import com.bumptech.glide.Glide
data class Contact(var _name : String, var _email : String) : BaseObservable() {
@get:Bindable
var name : String = _name
set(value) {
field = value
notifyPropertyChanged(BR.name)
}
@get : Bindable
var email : String = _email
set(value) {
field = value
notifyPropertyChanged(BR.email)
}
companion object {
@JvmStatic @BindingAdapter("profileImage")
fun loadImage(view : ImageView, imageUrl : String ) {
Glide.with(view.context)
.load(imageUrl)
.into(view)
}
}
}
In an Activity :
class MainActivity : AppCompatActivity() {
private lateinit var binding : ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.btnRoll.setOnClickListener {
rollDice()
}
}
private fun rollDice() {
val data = when((1..6).random()) {
1 -> R.drawable.dice_1
2 -> R.drawable.dice_2
3 -> R.drawable.dice_3
4 -> R.drawable.dice_4
5 -> R.drawable.dice_5
else -> R.drawable.dice_6
}
binding.imgDice.setImageResource(data)
}
}
In a Fragment :
class HomeFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = DataBindingUtil.inflate<FragmentHomeBinding>(inflater,
R.layout.fragment_home, container, false)
return binding.root
}
}
Paging
Used to gradually load the information on demand from the data source
Helps us in managing the activity and fragment life cycle
Lifecycle object uses Event and State enumeration to track the lifecycle status
Lifecycle owner provides lifecycle status to lifecycle aware components
class LifeCycleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.i(TAG, "LifecycleOwner ON_CREATE")
lifecycle.addObserver(LifeCycleActivityObserver())
}
companion object {
private val TAG : String = LifeCycleActivity::class.java.simpleName
}
}
class LifeCycleActivityObserver : LifecycleObserver {
companion object {
private val TAG : String = LifeCycleActivityObserver::class.java.simpleName
}
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreateEvent() {
Log.i(TAG, "Observer ON_CREATE")
}
}
Handles everything needed for in-app navigation
- Fixed start destination - This is the screen which the user sees when they launch the application from the launcher and also it has to be the last screen which user sees before returning to the launcher by pressing the back button
- Navigation state should be represented via stack of destinations - The navigation stack should have the start destination of the app at the bottom of the stack and the current destination at the top
- The up button should never exit the app - if the user is at start destination of the application, the up button should not be shown
- Up and back are identical within app's task - When we are on our own task in the application, the up and back button should have same functionality
- Handles Fragment Transaction
- Handles up and back actions correctly. Need not be handled manually
- Includes Navigation UI Patterns such as Bottom Navigation, Navigation Drawers with minimal boilerplate code
- Provides type safety when passing information - we use safeargs while using navigation components which provides type safety assess to the arguments
- Visualizing and editing navigation from Android studio Navigation editor
- Provides standardized resources for animations and transitions
private fun setUpBottomNav(navController: NavController) {Actions :
bottom_nav?.let {
NavigationUI.setupWithNavController(it, navController)
}
}
private fun setUpSideNav(navController: NavController) {
nav_view?.let {
NavigationUI.setupWithNavController(it, navController)
}
}
private fun setUpActionBar(navController: NavController) {
NavigationUI.setupActionBarWithNavController(this, navController, drawer_layout)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.toolbar_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val navController : NavController = Navigation.findNavController(this, R.id.nav_host_fragment)
val navigated : Boolean = NavigationUI.onNavDestinationSelected(item!!, navController)
return navigated || super.onOptionsItemSelected(item)
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(
Navigation.findNavController(this, R.id.nav_host_fragment), drawer_layout)
}
Navigation actions are connection between Destinations.
To add a navigation component, we need to go to the res folder and add Android Resource File and add a file and give a name navigation.xml and select Resource Type as Navigation from the dropdown. This creates a navigation folder inside res folder and the navigation.xml will be available within it. Add the fragments and make the navigation links as required for the flow between the fragments. If we need to make any fragment as HomeFragment, then we just need to select the fragment and click on the home option from the top menu.
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="androidx.navigation.fragment.NavHostFragment"
android:id="@+id/navControllerFragment"
app:defaultNavHost="true"
app:navGraph="@navigation/navigation" />
Comments
Post a Comment