A Beginner's Guide To Jetpack Architecture Components

A Beginner's Guide To Jetpack Architecture Components

Understanding and Utilizing Jetpack Architecture Components for Efficient Android Development.

Jetpack components can be seen as a compilation of Android libraries and approaches that incorporate best practices for efficient Android development. By utilizing these libraries, developers may cut down on boilerplate code and create code that is compatible with every version of Android. Jetpack was launched in 2018 by Google and inspired by Android support libraries, ensuring backward compatibility across all Android releases. The idea behind Jetpack Components was to handle changes or updates to data and the lifecycle of an application.

Jetpack components can be categorized into four types, but for this article, I'll focus on one: the architecture component. The four components are:

  1. Behavioral components

  2. UI components

  3. Architecture components

  4. Foundation components

Android Jetpack architecture component refers to a collection of architectural patterns and libraries that help developers build a robust, well-structured, and maintainable application. These libraries facilitate the separation of concerns, lifecycle management, data persistence, and other architectural principles. Architecture components can be classified into the following types:

  1. LiveData

  2. Navigation

  3. ViewModel

  4. Room

  5. Lifecycle

  6. WorkManager

  7. Paging

  8. DataBinding

For this article, we'll be taking a lot at only four components which are the LiveData, Navigation, ViewModel, and Room.

Jetpack Architecture Components

  1. LiveData: Observing Live Updates

LiveData is a data holder class that is observable. Different from the regular observable, LiveData class is lifecycle-aware, meaning that it is aware of and responds to the various stages or states of its lifecycle. The awareness of LiveData enables the component to handle operations like resource allocation, releasing resources, saving and restoring state, and reacting to configuration changes effectively. Consider a score counter app as an example. The app utilizes the LiveData component to hold the current score count in this scenario. The app maintains the score count within the LiveData object, and whenever the score changes, LiveData automatically notifies the user interface (UI) components. This notification triggers an update in the UI, ensuring that the latest score is displayed on the screen. By using LiveData as a lifecycle-aware mechanism, the app ensures that the UI stays synchronized with the score count throughout the various stages of the app's lifecycle.

Creating And Observing LiveData Objects

LiveData is a versatile wrapper that can be applied to various types of data, including objects that implement collections like List. An object of LiveData is normally stored in a ViewModel and accessible using a getter method. As an example, let's look at the following:

class NameViewModel: ViewModel() {

    //Create a LiveData using an int
   val currentScore: MutableLiveData<Int> by lazy {
        MutableLiveData<Int>()
    }
    // Rest of the ViewModel...
}

Observing a LiveData object in the onCreate() method is recommended as It prevents redundant calls from onResume() and ensures immediate display of data when the component becomes active. LiveData delivers updates efficiently, only when data changes or when an observer becomes active.

class NameActivity : AppCompatActivity() {

    // Delegate a Kotlin property using the 'by viewModels()' method
    // from the activity-ktx artifact
    private val model: NameViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Other code to set up the activity...

        // Implement the observer which updates the user interface.
        val scoreObserver = Observer<Int> { newName ->
            // The UI, in this case, a TextView, is updated.
            scoreTextView.text = newScore
        }

        // Provide the LifecycleOwner and observer for the LiveData, passing this activity as the LifecycleOwner
        model.currentScore.observe(this, scoreObserver)
    }
}

Updates can also be made to LiveData objects. Although there is no public way to update a LiveData object, the above scoreObserver can be updated as follows:

model.currentScore.setValue(newScore)
  1. Navigation

Navigation can be seen as the framework that allows users to transition between two destinations. This destination can be either fragments or activities. The navigation component helps in basic navigation implementation such as button clicks or a more complex pattern. It also helps with a consistent user experience. The navigation component also helps in structuring your app UI (User Interface) by following the single-activity principle. Additionally, as a component that supports Fragment wholly, it offers the usage of both the LifeCycle and ViewModel components and can manage the complexity of the Fragment Transactions.

This image was provided by Google

The navigation component has three parts namely :

  • Navigation Graph: The NavGraph is an XML resource that has all information related to navigation. This data can include content area (destinations), arguments, as well as the likely direction a user can assume.

  • NavHost: A container used to display the destinations of your app. This is an empty, distinctive container that incorporates a default implementation of the NavHost, NavHostFragment(a widget that displays various fragment destinations).

  • NavController: An object used for managing navigation within the navigation graph. It handles a seamless transition of content as users navigate through it.

When navigation takes place, communication between the navigation destination to the NavController occurs. It then displays the appropriate content to the NavHost.

  1. Room: Persisting Data

Room is an SQLite object-mapping library used for the local persistence of data (data are stored locally on phone memory). As an object mapping that encompasses SQLite, it provides an abstraction layer over SQLite, and it allows for concise database access and management. Highly recommended by Google over SQLite, Room converts queries into objects, checks for errors at compile time, and allows for a streamlined database management path. It also provides LiveData results for every given query.

There are there sub-components that make up the Room Component namely :

  • Entity: This is an annotated model data class that represents a table in a Room database. Each created instance of this class represents a row in the table of the app's database.

The code below defines a Data entity class.

@Entity
data class Person(
    @PrimaryKey val uid: Int,
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?
)
  • DAO (Data Access Object): This is a helper class that provides access to the database. The DAO is an interface responsible for providing the methods used by the user to interact, query, or perform database operations. The purpose of the DAO is to save the time needed to write direct queries (prone to error and hard to debug) while isolating the query logic from the database creation.

The below code illustrates a DAO.

@Dao
interface PersonDao{
    @Query("SELECT  FROM user")
    fun getAll(): List<User>

    @Query("SELECT  FROM user WHERE uid IN (:userIds)")
    fun loadAllByIds(userIds: IntArray): List<User>

    @Query("SELECT * FROM user WHERE first_name LIKE:first AND " +
           "last_name LIKE:last LIMIT 1")
    fun findByName(first: String, last: String): User

    @Insert
    fun insertAll(vararg users: User)

    @Delete
    fun delete(user: User)
}
  • Database: This is an abstract class that extends the Room database and serves as a connection to persisted data. It is responsible for exposing entities through the DAO (Data Access Object) and provides a convenient way to interact with the underlying database.

The code below defines a PersonDatabase class that holds the database. To meet the required conditions, the database class should be annotated with @Database, listing associated entities, be an abstract class extending RoomDatabase and define abstract methods for each associated DAO class.

    @Database(entities = [Person::class], version = 1)
    abstract class PersonDatabase: RoomDatabase() {
        abstract fun personDao(): PersonDao
    }

The image below shows the connection between the different Room sub-components:

  1. ViewModel

The ViewModel is a crucial component of the Android Jetpack Architecture Components that serves as a persistent holder for business logic and state at the screen level. It enables data persistence within the ViewModel and facilitates triggering ViewModel operations. Its primary purpose is to manage UI-related data and act as the primary source for exposing data.

The ViewModel is designed to address the challenge of Android configuration changes, such as screen rotations, where activities or fragments may be destroyed and recreated. By using a ViewModel, data can survive configuration changes. This prevents data loss that would otherwise occur if data were not properly saved and restored during such transitions.

The ViewModel is lifecycle aware, meaning it can automatically adjust its behavior and respond to lifecycle events of the associated activity or fragment. This capability allows it to handle operations such as data loading, API requests, and database transactions efficiently and consistently throughout the screen lifecycle.

Implementing and Accessing a ViewModel

Here is an example of how the ViewModel class is implemented.

data class TestUiState(
    val firstTestValue: Int? = null,
    val secondTestValue: Int? = null,
    val numberOfTest: Int = 0,
)

class TestViewModel : ViewModel() {

    // Expose screen UI state
    private val uiState = MutableStateFlow(TestUiState())
    val uiState: StateFlow<TestUiState> = uiState.asStateFlow()

    // Handle business logic
    fun showTest() {
        _uiState.update { currentState ->
            currentState.copy(
                firstTestValue= Random.nextInt(from = 1, until = 7),
                secondTestValue= Random.nextInt(from = 1, until = 7),
                numberOfTest= currentState.numberOfTest + 1,
            )
        }
    }
}

To access the ViewModel from an activity, you can use the following approach:

import androidx.activity.viewModels

class TestActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel when an activity's onCreate() method is called for the first time.
        // Re-created activities receive the same TestViewModel instance created by the first activity.

        // Delegate a Kotlin property using the 'by viewModels()' method

Implementation: Using the Jetpack Architecture Components

All Jetpack components rely on external libraries found in the Google Maven Repository. To add the Google repository, simply include google() in the repositories section of the dependencyResolutionManagement block in the settings.gradle file.

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        jcenter()
    }
}

Then you can add the required architectural components of your choice as shown below:

// 1: Room Components
def roomVersion = "1.1.1"
implementation "android.arch.persistence.room:runtime:$roomVersion"
kapt "android.arch.persistence.room:compiler:$roomVersion"

// 2: Lifecycle Components
def lifecycleVersion = "1.1.1"
implementation "android.arch.lifecycle:extensions:$lifecycleVersion"

// 3: Navigation Components
def navigationVersion = "1.0.0-alpha04"
implementation "android.arch.navigation:navigation-fragment-ktx:$navigationVersion"
implementation "android.arch.navigation:navigation-ui-ktx:$navigationVersion"

Benefits of the Jetpack Architecture Components

Below are the benefits of using the above-mentioned architecture components:

LiveData:

  • Prevents memory leaks.

  • Ensures appropriate UI updates.

  • Avoids crashes due to stopped activities.

  • Great for sharing resources.

Navigation:

  • Allows for easy transition of fragments.

  • Proper Handling of back actions.

  • Seamless implementation and handling of deep linking.

  • Easy integration of Navigation UI patterns with minimal effort.

  • Type-safe navigation and easy passing of data between destinations with the help of Safe Args.

Room:

  • Verification of SQL queries at Compile-Time.

  • Easy debugging and testing.

  • Convenient annotations to reduce boilerplate code.

  • Simplifies migration path for database access.

ViewModel:

  • Helps to persist the UI state.

  • Provides access to business logic.

  • Reduces bugs and crashes.

Best Practices and Resources for Jetpack Architecture Components

For further learning and resources, consider:

By following best practices and utilizing available resources, you can effectively leverage Jetpack Architecture Components in your Android development.

In Conclusion

Jetpack Architecture Components offer a set of libraries and best practices for efficient Android development. By understanding the components, leveraging lifecycle awareness, separating concerns, and minimizing boilerplate code, developers can build robust and maintainable apps. Refer to the provided resources to deepen your knowledge and explore further implementation examples. Embrace Jetpack Architecture Components to enhance your Android development experience.