Android

clean architecture mvvm

Mastering Clean Architecture: A Comprehensive Guide to Building Movies App with MVVM and Jetpack Compose

Clean Architecture and MVVM Architecture are two popular architectural patterns for building robust, maintainable, and scalable Android applications. In this article, we will discuss how to implement Clean Architecture and MVVM Architecture in an Android application using Kotlin. We will cover all aspects of both architectures in-depth and explain how they work together to create a robust application.

Clean Architecture

Clean Architecture is a software design pattern that emphasizes separation of concerns and the use of dependency injection. It divides an application into layers, with each layer having a specific responsibility. The layers include:

  1. Presentation Layer
  2. Domain Layer
  3. Data Layer

The Presentation Layer is responsible for the user interface and interacts with the user. The Domain Layer contains business logic and rules. The Data Layer interacts with external sources of data.

The Clean Architecture pattern is designed to promote testability, maintainability, and scalability. It reduces coupling between different parts of an application, making it easier to modify or update them without affecting other parts of the application.

MVVM Architecture

MVVM stands for Model-View-ViewModel. It is a software design pattern that separates an application into three layers: Model, View, and ViewModel. The Model represents the data and business logic. The View represents the user interface. The ViewModel acts as a mediator between the Model and the View. It exposes data from the Model to the View and handles user input from the View.

MVVM Architecture promotes separation of concerns, testability, and maintainability. It is designed to work with data binding and makes it easy to update the user interface when data changes.

Combining Clean and MVVM Architecture

Clean Architecture and MVVM Architecture can be used together to create a robust, maintainable, and scalable Android application. The Presentation Layer in Clean Architecture corresponds to the View and ViewModel in MVVM Architecture. The Domain Layer in Clean Architecture corresponds to the Model in MVVM Architecture. The Data Layer in Clean Architecture corresponds to the Data Layer in MVVM Architecture.

Implement Clean and MVVM Architecture

Let’s build one demo app to implement Clean and MVVM Architecture. we will create a simple app that displays a list of movies and allows the user to view the details of each movie. We will use the Movie Database API as our data source.

Building this MVVM demo app using Clean Architecture, MVVM, Kotlin, Coroutines, Room, Hilt, Retrofit, Moshi, Flow, and Jetpack Compose.

Set up the project

Create a new project in Android Studio and add the necessary dependencies for MVVM Architecture, such as room, hilt, and ViewModel.

build.gradle (Module:app)

Kotlin
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}

android {
    namespace 'com.softaai.mvvmdemo'
    compileSdk 33

    defaultConfig {
        applicationId "com.softaai.mvvmdemo"
        minSdk 24
        targetSdk 33
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary true
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.4.2'
    }
    packagingOptions {
        resources {
            excludes += '/META-INF/{AL2.0,LGPL2.1}'
        }
    }
}

dependencies {

    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.core:core-ktx:1.10.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.8.0'
    implementation 'androidx.compose.material3:material3:1.1.0-beta02'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'

    // activity
    implementation 'androidx.activity:activity-ktx:1.7.0'
    implementation 'androidx.activity:activity-compose:1.7.0'

    // Lifecycle
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
    implementation "androidx.lifecycle:lifecycle-common:2.6.1"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
    implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"

    //compose
    implementation 'androidx.compose.ui:ui:1.5.0-alpha02'
    implementation 'androidx.compose.material:material:1.5.0-alpha02'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
    debugImplementation "androidx.compose.ui:ui-tooling:1.5.0-alpha02"
    implementation "androidx.compose.ui:ui-tooling-preview:1.5.0-alpha02"
    implementation "androidx.compose.runtime:runtime-livedata:1.5.0-alpha02"


    //compose navigation
    implementation "androidx.navigation:navigation-compose:2.5.3"
    implementation "androidx.hilt:hilt-navigation-compose:1.0.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1"


    // Dagger hilt
    implementation 'com.google.dagger:hilt-android:2.45'
    kapt 'com.google.dagger:hilt-compiler:2.45'


    // Networking
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
    implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.2'
    implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.2'


    // Moshi
    implementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
    kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.14.0'


    // Coroutine
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1"


    // Room and room pagination
    implementation "androidx.room:room-runtime:2.5.1"
    kapt "androidx.room:room-compiler:2.5.1"
    implementation "androidx.room:room-ktx:2.5.1"
    implementation "androidx.room:room-paging:2.5.1"


    // coil image loading
    implementation 'io.coil-kt:coil-compose:2.3.0'

    // multidex
    implementation 'androidx.multidex:multidex:2.0.1'


    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.5.0-alpha02'
    debugImplementation 'androidx.compose.ui:ui-tooling:1.5.0-alpha02'
    debugImplementation 'androidx.compose.ui:ui-test-manifest:1.5.0-alpha02'
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10"
    //if update warning comes then go to settings and install latest plugin and restart

}

Define Initial Packages

Create initial packages for each layer of Clean Architecture: Presentation, Domain, and Data. Inside each package, create sub-packages for specific functionalities of the layer.

├── data
│   ├── repository
│   └── source
│       ├── local
│       │   ├── datastore
│       │   └── roomdb
│       └── remote
├── di
│   ├── movies
│   └── moviedetails
├── domain
│   ├── model
│   ├── repository
│   └── usecase
└── presentation
    ├── ui
    └── viewmodel

In this hierarchy, we have:

  • data package which contains the repository and source packages.
  • The repository package contains classes responsible for fetching data from source and returning it to domain.
  • The source package contains local and remote packages.
  • The local package contains classes responsible for accessing data from local data storage, such as datastore and roomdb.
  • The remote package contains classes responsible for accessing data from remote data storage, such as APIs.
  • di package which contains the movies and moviedetails packages.
  • These packages contain classes responsible for dependency injection related to movies and moviedetails modules.
  • domain package which contains the model, repository, and usecase packages.
  • The model package contains classes representing the data model of the application.
  • The repository package contains interfaces defining the methods that the repository classes in data package must implement.
  • The usecase package contains classes responsible for defining the use cases of the application, by using repository interfaces and returning the result to the presentation layer.
  • presentation package which contains the ui and viewmodel packages.
  • The ui package contains classes responsible for the user interface of the application, such as activities, fragments, and views.
  • The viewmodel package contains classes responsible for implementing the ViewModel layer of the application, which holds data related to the UI and communicates with the usecase layer.

Identify JSON Response

Identify the JSON response from the URL, Before making a network request to the URL, please use your own API Key as mine is an invalid key. Then examine the JSON response using an Online JSON Viewer to identify its structure. Once you have identified the structure of the response, create Kotlin DTOs for the response and place them in the remote package.

DTOs, Entities, and Domain Models:

In our Android application, we will have different types of data models. These models include DTOs, Entities, and Domain Models.

DTOs (Data Transfer Objects) are used to transfer data between different parts of the application. They are typically used to communicate with a remote server or API.

Entities represent the data models in our local database. They are used to persist data in our application.

Domain Models represent the business logic in our application. They contain the logic and rules that govern how data is processed in the application.

By using these different types of models, we can separate our concerns and ensure that each model is responsible for its own functionality. This makes our code more modular and easier to maintain.

Mapper Functions:

In our Android application, we will often need to convert between different types of models. For example, we might need to convert a DTO to an Entity or an Entity to a Domain Model. To do this, we can use Mapper Functions.

Mapper Functions are used to convert data between different models. They take an input model and convert it to an output model. By using Mapper Functions, we can ensure that our code is organized and maintainable, and we can easily convert between different models as needed.

Define DTOs

We can create Kotlin data transfer objects (DTOs) to represent the data and place them into the remote package, as it represents data fetched from a remote data source

Kotlin
package com.softaai.mvvmdemo.data.source.remote.dto


import com.softaai.mvvmdemo.data.source.local.roomdb.entity.PopularMoviesEntity
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class PopularMoviesDto(
    @Json(name = "page")
    val page: Int,
    @Json(name = "results")
    val results: List<MovieDto>,
    @Json(name = "total_pages")
    val totalPages: Int,
    @Json(name = "total_results")
    val totalResults: Int
) {
    fun toPopularMoviesEntity(): PopularMoviesEntity {
        return PopularMoviesEntity(
            page = page,
            results = results.map { it.toMovieEntity() }
        )
    }
}
Kotlin
package com.softaai.mvvmdemo.data.source.remote.dto


import com.softaai.mvvmdemo.data.source.local.roomdb.entity.MovieEntity
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class MovieDto(
    @Json(name = "adult")
    val adult: Boolean,
    @Json(name = "backdrop_path")
    val backdropPath: String,
    @Json(name = "genre_ids")
    val genreIds: List<Int>,
    @Json(name = "id")
    val id: Int,
    @Json(name = "original_language")
    val originalLanguage: String,
    @Json(name = "original_title")
    val originalTitle: String,
    @Json(name = "overview")
    val overview: String,
    @Json(name = "popularity")
    val popularity: Double,
    @Json(name = "poster_path")
    val posterPath: String,
    @Json(name = "release_date")
    val releaseDate: String,
    @Json(name = "title")
    val title: String,
    @Json(name = "video")
    val video: Boolean,
    @Json(name = "vote_average")
    val voteAverage: Double,
    @Json(name = "vote_count")
    val voteCount: Int
) {
    fun toMovieEntity(): MovieEntity {
        return MovieEntity(
            id = id,
            title = title,
            overview = overview,
            posterUrl = posterPath,
            releaseDate = releaseDate
        )
    }
}

Define the data source

To fetch movies from the Movie Database API, we will use Retrofit to define an interface that defines the API endpoints. We will also use Moshi to deserialize the JSON responses into our Movie data class. Here\’s an example of how to define the API interface:

Kotlin
package com.softaai.mvvmdemo.data.source.remote

import com.softaai.mvvmdemo.data.source.remote.dto.PopularMoviesDto
import retrofit2.http.GET


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */

interface MovieApiService {

    @GET("movie/popular")
    suspend fun getPopularMovies(): PopularMoviesDto

    companion object {
        const val BASE_URL: String = "https://api.themoviedb.org/3/"
    }
}

Here, we are using the @GET annotation to define the API endpoint, and the suspend keyword to indicate that this function should be called from a coroutine. We are also using the deserialized PopularMovieDto data class.

Note → We used PopularMoviesDto data class directly instead of wrapping it in a Response or Resource class. This is because it is assumed that the API response will always contain the expected data structure and any errors in the API call will be handled by catching exceptions, another reason is we are not tightly coupling our app to the API response structure and can modify the response format without affecting the rest of the app.

Define Resource Sealed Class

Resource Sealed Classes are used to represent the state of a request or operation that can either succeed or fail. They allow us to handle different states of an operation, such as loading, success, or error, in a more organized way. Typically, a Resource Sealed Class contains three states:

  1. Loading: When the operation is in progress.
  2. Success: When the operation is successful and data is available.
  3. Error: When the operation fails.
Kotlin
package com.softaai.mvvmdemo.data.source.remote


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */
sealed class Resource<T>(val data: T? = null, val message: String? = null) {
    class Loading<T>(data: T? = null) : Resource<T>(data)
    class Success<T>(data: T?) : Resource<T>(data)
    class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
}


By using Resource Sealed Classes, we can easily handle different states of an operation in our ViewModel without writing lots of boilerplate code.

Implement Interceptor for network requests

The purpose of the interceptor is to add an API key query parameter to every outgoing network request.

Kotlin
package com.softaai.mvvmdemo.data.source.remote

import okhttp3.Interceptor
import okhttp3.Response


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */
class RequestInterceptor : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val newUrl = originalRequest.url
            .newBuilder()
            .addQueryParameter(
                "api_key",
                "04a03ff73803441c785b1ae76dbdab9c"    //TODO Use your api key this one invalid
            )
            .build()
        val request = originalRequest.newBuilder()
            .url(newUrl)
            .build()
        return chain.proceed(request)
    }
}

The RequestInterceptor class implements the Interceptor interface provided by the OkHttp library, which allows it to intercept and modify HTTP requests and responses.

In the intercept method, the incoming chain parameter represents the chain of interceptors and the final network call to be executed. The method first retrieves the original request from the chain using chain.request(). It then creates a new URL builder from the original request URL and adds a query parameter with the key \”api_key\” and a specific value to it. Here use your own api_key as existing key is invalid

Next, it creates a new request by calling originalRequest.newBuilder() and setting the new URL with the added query parameter using .url(newUrl). Finally, it calls chain.proceed(request) to execute the modified request and return the response.

Overall, this interceptor helps to ensure that every network request made by the app includes a valid API key, which is required for authentication and authorization purposes.

Implementation of Room

Room is a powerful ORM (Object-Relational Mapping) library that makes it easy to work with a SQLite database in Android. It provides a high-level API for working with database tables, queries, and transactions, as well as support for Flow, LiveData, and RxJava for reactive programming.

Define the Entity

First, we’ll define the MovieEntity class, which represents the movies table in our local database. We annotate the class with @Entity and specify the table name and primary key. We also define the columns using public properties.

Kotlin
package com.softaai.mvvmdemo.data.source.local.roomdb.entity

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.softaai.mvvmdemo.domain.model.Movie


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */
@Entity(tableName = MovieEntity.TABLE_NAME)
data class MovieEntity(
    @PrimaryKey val id: Int,
    val title: String,
    val overview: String,
    @ColumnInfo(name = "poster_url") val posterUrl: String,
    @ColumnInfo(name = "release_date") val releaseDate: String
) {

    fun toMovie(): Movie {
        return Movie(
            title = title,
            overview = overview,
            posterUrl = posterUrl,
            releaseDate = releaseDate
        )
    }

    companion object {
        const val TABLE_NAME = "movie"
    }
}

We have another entity for API response, which contains a movie entity list

Kotlin
package com.softaai.mvvmdemo.data.source.local.roomdb.entity

import androidx.room.Entity
import androidx.room.PrimaryKey
import com.softaai.mvvmdemo.domain.model.PopularMovies


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */
@Entity(tableName = PopularMoviesEntity.TABLE_NAME)
class PopularMoviesEntity(
    @PrimaryKey(autoGenerate = true)
    val primaryKeyId: Int? = null,
    val page: Int,
    val results: List<MovieEntity>
) {

    fun toPopularMovies(): PopularMovies {
        return PopularMovies(
            page = page,
            results = results.map { it.toMovie() }
        )
    }

    companion object {
        const val TABLE_NAME = "popular_movies"
    }
}

Define the DAO

Next, we’ll define the MovieDao interface, which provides the methods to interact with the movies table. We annotate the interface with @Dao and define the query methods using annotations such as @Query, @Insert, and @Delete.

Kotlin
package com.softaai.mvvmdemo.data.source.local.roomdb.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.softaai.mvvmdemo.data.source.local.roomdb.entity.MovieEntity


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */

@Dao
interface MovieDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertMovieList(movies: List<MovieEntity>)

    @Query("SELECT * FROM ${MovieEntity.TABLE_NAME}")
    suspend fun getMovieList(): List<MovieEntity>

    @Query("DELETE FROM ${MovieEntity.TABLE_NAME}")
    suspend fun deleteAll()
}

Note ->Here also, we are not using any wrappers like Flow, Response, or Resource. The reason behind this is that we are keeping the repository layer decoupled from the data sources (local or remote) and allowing for easier testing and evolution. In this specific case, it is a simple synchronous database operation, as the data is being retrieved from the local database using Room. Room already provides the functionality to perform asynchronous database operations in the background, so we do not need to use any additional wrappers like Flow or Resource. We can simply call the getMovieList() method from a coroutine and retrieve the list of MovieEntity objects.

Define Type Converter

In Room, a type converter is a way to convert non-primitive types (such as Date or custom objects in our case List<MovieEntity>) to primitive types that can be stored in the SQLite database.

To use a type converter in Room, you need to create a class that implements the TypeConverter interface, which has two methods: toType() and fromType(). The toType() method converts a non-primitive type to a primitive type, while the fromType() method converts the primitive type back to the non-primitive type.

Kotlin
package com.softaai.mvvmdemo.data.source.local.roomdb.converter

import androidx.room.TypeConverter
import com.softaai.mvvmdemo.data.source.local.roomdb.entity.MovieEntity
import com.softaai.mvvmdemo.domain.model.Movie
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */
class PopularMoviesEntityConverter {

    @TypeConverter
    fun fromStringToMovieList(value: String): List<Movie>? =
        Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build()
            .adapter<List<Movie>>(Types.newParameterizedType(List::class.java, Movie::class.java))
            .fromJson(value)

    @TypeConverter
    fun fromMovieListTypeToString(movieListType: List<Movie>?): String =
        Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build()
            .adapter<List<Movie>>(Types.newParameterizedType(List::class.java, Movie::class.java))
            .toJson(movieListType)


    @TypeConverter
    fun fromStringToMovieEntityList(value: String): List<MovieEntity>? =
        Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build().adapter<List<MovieEntity>>(
                Types.newParameterizedType(
                    List::class.java,
                    MovieEntity::class.java
                )
            ).fromJson(value)

    @TypeConverter
    fun fromMovieEntityListTypeToString(movieEntityListType: List<MovieEntity>?): String =
        Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build().adapter<List<MovieEntity>>(
                Types.newParameterizedType(
                    List::class.java,
                    MovieEntity::class.java
                )
            ).toJson(movieEntityListType)

}

To use this TypeConverter, you need to annotate the field or property that needs to be converted with the @TypeConverters annotation, specifying the converter class.

Define the Database

Finally, we’ll define the MovieDatabase class, which represents the entire local database. We annotate the class with @Database and specify the list of entities and version number. We also define a singleton instance of the database using the Room.databaseBuilder method.

Kotlin
package com.softaai.mvvmdemo.data.source.local.roomdb

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.softaai.mvvmdemo.data.source.local.roomdb.converter.PopularMoviesEntityConverter
import com.softaai.mvvmdemo.data.source.local.roomdb.dao.MovieDao
import com.softaai.mvvmdemo.data.source.local.roomdb.dao.PopularMoviesDao
import com.softaai.mvvmdemo.data.source.local.roomdb.entity.MovieEntity
import com.softaai.mvvmdemo.data.source.local.roomdb.entity.PopularMoviesEntity


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */

@Database(
    entities = [PopularMoviesEntity::class, MovieEntity::class],
    version = 1,
    exportSchema = false
)
@TypeConverters(PopularMoviesEntityConverter::class)
abstract class MovieDatabase : RoomDatabase() {
    abstract fun getMovieDao(): MovieDao

    abstract fun getPopularMoviesDao(): PopularMoviesDao

    companion object {
        @Volatile
        private var INSTANCE: MovieDatabase? = null

        fun getDatabase(context: Context): MovieDatabase {
            val tempInstance = INSTANCE
            if (tempInstance != null) {
                return tempInstance
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    MovieDatabase::class.java,
                    "movie_database"
                ).build()
                INSTANCE = instance
                return instance
            }
        }
    }
}

Define Repository interface in Domain Layer

Usually In the Domain Layer, we define the interfaces for the Repository and Use Case (In our case, skipped for the Use Case). These interfaces define the methods that will be used to interact with the data layer. The Repository interface defines the methods that will be used to retrieve and save data, while the Use Case interface defines the business logic that will be performed on the data.

We will create a MovieRepository interface that defines the methods for fetching movies:

Kotlin
package com.softaai.mvvmdemo.domain.repository

import com.softaai.mvvmdemo.data.source.remote.Resource
import com.softaai.mvvmdemo.domain.model.Movie
import kotlinx.coroutines.flow.Flow


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */
interface MovieRepository {
    fun getPopularMovies(): Flow<Resource<List<Movie>>>
}

We are returning a Flow<Resource<List<Movie>>> from the getPopularMovies() function. The Flow will emit the result of the API call asynchronously, and the Resource class will hold either the list of movies or an error.

Implement a Repository interface in the Data Layer

We define interfaces for the Repository and Use Case in the Domain Layer and these interfaces will be implemented in the Data Layer. By separating the interfaces from their implementations, we can easily swap out the data layer implementation if needed. This allows us to easily switch between different data sources, such as a local database or a remote API, without having to modify the business logic layer.

In our example, we will create an implementation of the MovieRepository interface that uses Retrofit and Moshi to fetch the popular movies:

Kotlin
package com.softaai.mvvmdemo.data.repository

import com.softaai.mvvmdemo.data.source.local.roomdb.dao.MovieDao
import com.softaai.mvvmdemo.data.source.local.roomdb.dao.PopularMoviesDao
import com.softaai.mvvmdemo.data.source.remote.MovieApiService
import com.softaai.mvvmdemo.data.source.remote.Resource
import com.softaai.mvvmdemo.domain.model.Movie
import com.softaai.mvvmdemo.domain.repository.MovieRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.HttpException
import java.io.IOException


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */
class MovieRepositoryImpl constructor(
    private val movieApiService: MovieApiService,
    private val popularMoviesDao: PopularMoviesDao,
    private val movieDao: MovieDao
) : MovieRepository {

    override fun getPopularMovies(): Flow<Resource<List<Movie>>> = flow {
        emit(Resource.Loading())
        try {
            fetchAndInsertPopularMovies(movieApiService, popularMoviesDao, movieDao)
        } catch (e: HttpException) {
            emit(
                Resource.Error(
                    message = "Oops, something went wrong!"
                )
            )
        } catch (e: IOException) {
            emit(
                Resource.Error(
                    message = "Couldn't reach server, check your internet connection."
                )
            )
        }
        // single source of truth we will emit data from db only and not directly from remote
        emit(Resource.Success(getPopularMoviesFromDb(movieDao)))
    }


    private suspend fun fetchAndInsertPopularMovies(
        movieApiService: MovieApiService,
        popularMoviesDao: PopularMoviesDao,
        movieDao: MovieDao
    ) {

        val remotePopularMovies = movieApiService.getPopularMovies()
        popularMoviesDao.insertPopularMovies(remotePopularMovies.toPopularMoviesEntity())
        movieDao.insertMovieList(remotePopularMovies.results.map { it.toMovieEntity() }) //now insert newly fetched data to db

    }


    private suspend fun getPopularMoviesFromDb(movieDao: MovieDao): List<Movie> {
        val newPopularMovies = movieDao.getMovieList().map { it.toMovie() }
        return newPopularMovies
    }


}

Here, we are using the flow builder from the Kotlin coroutines library to emit the result of the API call asynchronously. We are also using the catch operator to catch any exceptions that might occur during the API call. If there is an error, we emit the error wrapped in the Resource.Error class.

Implement Use Case

I skipped implementing the Use Case in the Data Layer, such as the Repository, and instead implemented it directly in the Domain Layer for this small assignment. However, in a bigger project, it is important to implement it properly in the Data Layer.

Kotlin
package com.softaai.mvvmdemo.domain.usecase

import com.softaai.mvvmdemo.data.source.remote.Resource
import com.softaai.mvvmdemo.domain.model.Movie
import com.softaai.mvvmdemo.domain.repository.MovieRepository
import kotlinx.coroutines.flow.Flow


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */
class GetPopularMovies(
    private val movieRepository: MovieRepository
) {
    operator fun invoke(): Flow<Resource<List<Movie>>> {
        return movieRepository.getPopularMovies()
    }
}

The GetPopularMovies class is a use case class in the domain layer that provides a way to retrieve a list of popular movies from the MovieRepository. By using this class, we can easily retrieve the list of popular movies by calling its invoke() method an operator function, which returns a Flow. We can then collect the items emitted by the Flow and handle the different states of the data using the Resource class.

Add Hilt Modules for Dependency Injection

Hilt is a dependency injection framework that makes it easy to manage dependencies in Android apps. It is built on top of Dagger, a popular dependency injection library, and provides a simpler, more streamlined API for configuring and injecting dependencies.

To inject dependencies into our ViewModel and Repository, we’ll use Hilt for Dependency Injection. Since we have already added the Hilt dependency in the gradle file, we can now directly annotate our Application class with @HiltAndroidApp:

Kotlin
package com.softaai.mvvmdemo

import android.app.Application
import dagger.hilt.android.HiltAndroidApp


/**
 * Created by amoljp19 on 4/19/2023.
 * softAai Apps.
 */

@HiltAndroidApp
class MvvmDemoApp : Application() {
}

Define Hilt modules

Create a Kotlin object for each module and annotate it with @Module. In each module, define one or more provider methods that create instances of your dependencies and annotate them with @Provides.

MoviesNetworkModule

Kotlin
package com.softaai.mvvmdemo.di.moviesmodule

import com.softaai.mvvmdemo.data.source.remote.MovieApiService
import com.softaai.mvvmdemo.data.source.remote.RequestInterceptor
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */
@Module
@InstallIn(SingletonComponent::class)
class MoviesNetworkModule {

    private val interceptor = run {
        val httpLoggingInterceptor = HttpLoggingInterceptor()
        httpLoggingInterceptor.apply {
            httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
        }
    }

    @Provides
    @Singleton
    fun provideOkHttpClient() =
        OkHttpClient.Builder().addInterceptor(RequestInterceptor()).addInterceptor(interceptor)
            .build()


    @Singleton
    @Provides
    fun provideRetrofitService(okHttpClient: OkHttpClient): MovieApiService =
        Retrofit.Builder()
            .baseUrl(MovieApiService.BASE_URL)
            .addConverterFactory(
                MoshiConverterFactory.create(
                    Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
                )
            )
            .client(okHttpClient)
            .build()
            .create(MovieApiService::class.java)

}

This is a Hilt module called MoviesNetworkModule, which is used for providing dependencies related to network communication with the MovieApiService. The module is annotated with @Module and @InstallIn(SingletonComponent::class), which means that it will be installed in the SingletonComponent and has the scope of the entire application.

The module provides the following dependencies:

  • OkHttpClient: This dependency is provided by a method called provideOkHttpClient, which returns an instance of OkHttpClient that is built with RequestInterceptor and HttpLoggingInterceptor.
  • MovieApiService: This dependency is provided by a method called provideRetrofitService, which takes an instance of OkHttpClient as a parameter and returns an instance of MovieApiService. This method builds a Retrofit instance using MoshiConverterFactory for JSON parsing and the provided OkHttpClient, and creates a MovieApiService instance using the Retrofit.create method.

The @Singleton annotation is used on both provideOkHttpClient and provideRetrofitService methods, which means that Hilt will only create one instance of each dependency and provide it whenever it is needed.

By using these @Provides methods, we can provide these dependencies to any component in our app by simply annotating the constructor of that component with @Inject.

MoviesDatabaseModule

Kotlin
package com.softaai.mvvmdemo.di.moviesmodule

import android.app.Application
import com.softaai.mvvmdemo.data.source.local.roomdb.MovieDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */
@Module
@InstallIn(SingletonComponent::class)
class MoviesDatabaseModule {

    @Singleton
    @Provides
    fun provideDatabase(application: Application) = MovieDatabase.getDatabase(application)

    @Singleton
    @Provides
    fun providePopularMoviesDao(database: MovieDatabase) =
        database.getPopularMoviesDao()

    @Singleton
    @Provides
    fun provideMovieDao(database: MovieDatabase) =
        database.getMovieDao()
}

Here we have defined a Hilt module called MoviesDatabaseModule which is annotated with @Module and @InstallIn(SingletonComponent::class). This means that this module will be installed in the SingletonComponent which has the scope of the entire application. By using these @Provides methods, we can provide these dependencies to any component in our app by simply annotating the constructor of that component with @Inject.

For example, if we want to use PopularMoviesDao in our MovieRepository, we can simply annotate the constructor of MovieRepository with @Inject and pass PopularMoviesDao as a parameter:

Kotlin
class MovieRepository @Inject constructor(
    private val movieApiService: MovieApiService,
    private val popularMoviesDao: PopularMoviesDao,
    private val movieDao: MovieDao
) {
    ...
}

By doing this, Hilt will automatically provide the PopularMoviesDao, MovieDao, and MovieApiService objects to our MovieRepository whenever it is needed.

MoviesRepositoryModule

Kotlin
package com.softaai.mvvmdemo.di.moviesmodule

import com.softaai.mvvmdemo.data.repository.MovieRepositoryImpl
import com.softaai.mvvmdemo.data.source.local.roomdb.dao.MovieDao
import com.softaai.mvvmdemo.data.source.local.roomdb.dao.PopularMoviesDao
import com.softaai.mvvmdemo.data.source.remote.MovieApiService
import com.softaai.mvvmdemo.domain.repository.MovieRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */
@Module
@InstallIn(SingletonComponent::class)
class MoviesRepositoryModule {

    @Provides
    @Singleton
    fun provideMovieRepositoryImpl(
        movieApiService: MovieApiService,
        popularMoviesDao: PopularMoviesDao,
        movieDao: MovieDao
    ): MovieRepository = MovieRepositoryImpl(movieApiService, popularMoviesDao, movieDao)

}

This is a Dagger Hilt module for providing the MovieRepository implementation to the app. The module is annotated with @Module and @InstallIn(SingletonComponent::class) which means that the MovieRepository will have a singleton scope throughout the app.

The @Provides method is defined to provide the MovieRepositoryImpl instance. This method takes three parameters: movieApiService of type MovieApiService, popularMoviesDao of type PopularMoviesDao, and movieDao of type MovieDao. These dependencies are injected into the constructor of MovieRepositoryImpl to create its instance.

MoviesUseCaseModule

Kotlin
package com.softaai.mvvmdemo.di.moviesmodule

import com.softaai.mvvmdemo.domain.repository.MovieRepository
import com.softaai.mvvmdemo.domain.usecase.GetPopularMovies
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */
@Module
@InstallIn(SingletonComponent::class)
class MoviesUsecaseModule {

    @Provides
    @Singleton
    fun provideGetPopularMoviesUseCase(repository: MovieRepository): GetPopularMovies =
        GetPopularMovies(repository)
}

The GetPopularMovies use case by injecting the MovieRepository. The module is annotated with @InstallIn(SingletonComponent::class) which means it will be installed in the SingletonComponent of the application.

The provideGetPopularMoviesUseCase method is annotated with @Provides and @Singleton, indicating that it provides a singleton instance of the GetPopularMovies use case.

The repository parameter of the method is injected via constructor injection, as it is declared as a dependency of the GetPopularMovies constructor. The MovieRepository is provided by the MoviesRepositoryModule which is also installed in the SingletonComponent.

Define the ViewModel

Now, we can define the ViewModel that will be used to expose the movie data to the UI. We will create a MoviesViewModel class that extends the ViewModel class from the Android Architecture Components library:

Kotlin
package com.softaai.mvvmdemo.presentation.viewmodel

import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.softaai.mvvmdemo.data.source.remote.Resource
import com.softaai.mvvmdemo.domain.usecase.GetPopularMovies
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */
@HiltViewModel
class MoviesViewModel @Inject constructor(
    private val getPopularMovies: GetPopularMovies
) : ViewModel() {

    private val _state = mutableStateOf(MovieUiState())
    val state: State<MovieUiState> = _state

    init {
        getMovies()
    }

    fun getMovies() {
        viewModelScope.launch {

            getPopularMovies().onEach { result ->
                when (result) {
                    is Resource.Loading -> {
                        _state.value = state.value.copy(
                            moviesList = result.data ?: emptyList(),
                            isLoading = true
                        )
                    }
                    is Resource.Success -> {
                        _state.value = state.value.copy(
                            moviesList = result.data ?: emptyList(),
                            isLoading = false
                        )
                    }
                    is Resource.Error -> {
                        _state.value = state.value.copy(
                            moviesList = result.data ?: emptyList(),
                            isLoading = false
                        )

                    }
                }
            }.launchIn(this)
        }
    }
}

This is the implementation of the MoviesViewModel, which is responsible for fetching and providing the list of popular movies to the UI layer. It uses the GetPopularMovies use case to fetch the data from the repository and updates the UI state based on the result of the operation.

Kotlin
package com.softaai.mvvmdemo.presentation.viewmodel

import com.softaai.mvvmdemo.domain.model.Movie


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */
data class MovieUiState(
    val moviesList: List<Movie> = emptyList(),
    val isLoading: Boolean = false
)

The @HiltViewModel annotation is used to inject dependencies into a ViewModel using Hilt. When a ViewModel is annotated with @HiltViewModel, Hilt generates a factory for the ViewModel and provides dependencies to the ViewModel via this factory. This way, the ViewModel can easily access dependencies, such as use cases or repositories, without the need to manually create and inject them.

The ViewModel uses a mutableStateOf() function to create a state object that can be updated from anywhere in the ViewModel. The state object is exposed as an immutable State object to the UI layer, which can observe it and update the UI accordingly.

The ViewModel also uses the viewModelScope to launch a coroutine that executes the use case, and observes the result of the operation using the onEach operator. Based on the result, the ViewModel updates the UI state accordingly, indicating whether the data is being loaded, whether it has been loaded successfully, or whether an error has occurred.

Define the Compose UI

First we define a MovieItem composable that displays a single movie item in a row. We are using CoilImage from the Coil library to display the movie poster image, and Row and Column composable functions to create the layout.

Kotlin
package com.softaai.mvvmdemo.presentation.ui.compose

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.softaai.mvvmdemo.domain.model.Movie


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */

@Composable
fun MovieItem(
    movie: Movie,
    onItemClick: (Movie) -> Unit
) {
    Row(
        modifier = Modifier
            .clickable { onItemClick(movie) }
            .padding(vertical = 16.dp, horizontal = 16.dp)
            .fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically
    ) {

        CoilImage(imageUrl = movie.posterUrl)

        Spacer(modifier = Modifier.width(16.dp))
        Column {
            Text(
                text = movie.title,
                fontWeight = FontWeight.Bold,
                fontSize = 20.sp,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis
            )
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = movie.releaseDate,
                fontWeight = FontWeight.Normal,
                fontSize = 16.sp,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis
            )
        }
    }
}


CoilImage Composable Function

Kotlin
package com.softaai.mvvmdemo.presentation.ui.compose

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import coil.compose.rememberImagePainter
import com.softaai.mvvmdemo.R


/**
 * Created by amoljp19 on 4/19/2023.
 * softAai Apps.
 */
@Composable
fun CoilImage(imageUrl: String) {
    Image(
        painter = rememberImagePainter(
            data = "https://image.tmdb.org/t/p/w600_and_h900_bestv2${imageUrl}",
            builder = {
                // Optional: Add image transformations
                placeholder(R.drawable.ic_launcher_foreground)
            }
        ),
        contentDescription = "Coil Image",
        modifier = Modifier
            .size(80.dp)
            .clip(RoundedCornerShape(4.dp))
    )
}

A composable function called CoilImage, displays an image using Coil library in Jetpack Compose. The function takes a String parameter called imageUrl which is the URL of the image to be displayed.

Finally, we will create a MoviesListScreen composable function that displays a list of popular movies using a LazyColumn.

Kotlin
package com.softaai.mvvmdemo.presentation.ui.compose

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.softaai.mvvmdemo.presentation.viewmodel.MoviesViewModel


/**
 * Created by amoljp19 on 4/18/2023.
 * softAai Apps.
 */
@Composable
fun MovieListScreen(moviesViewModel: MoviesViewModel = hiltViewModel()) {

    val state = moviesViewModel.state.value

    LazyColumn(
        Modifier.fillMaxSize(),
        contentPadding = PaddingValues(bottom = 16.dp)
    ) {
        items(state.moviesList.size) { i ->
            MovieItem(movie = state.moviesList[i], onItemClick = {})
        }
    }
}

The Composable function MovieListScreen which takes a MoviesViewModel as a parameter and sets its default value using the hiltViewModel() function provided by the Hilt library that allows you to retrieve a ViewModel instance that is scoped to the current Compose component. This is useful because it allows you to inject dependencies directly into your ViewModel using the Hilt dependency injection system.

By using hiltViewModel() instead of creating a new instance of the MoviesViewModel class manually, you ensure that the instance of the MoviesViewModel used in the MovieListScreen composable is the same instance that is injected by Hilt into the ViewModel.

Inside the function, it gets the current state of the ViewModel using moviesViewModel.state.value and stores it in a variable called state.

It then creates a LazyColumn with Modifier.fillMaxSize() and a content padding of PaddingValues(bottom = 16.dp). Inside the LazyColumn, it creates a list of items using the items function, which iterates over the state.moviesList and creates a MovieItem for each movie.

The MovieItem composable is passed the movie object from the current iteration, and an empty lambda function onItemClick (which could be used to handle clicks on the item).

Putting it all together

Now, we can put all the pieces together in our MainActivity, which is annotated with the @AndroidEntryPoint annotation. This annotation is part of the Hilt library, and it allows Hilt to generate a component for the activity and provide dependencies to its fields and methods.

Kotlin
package com.softaai.mvvmdemo

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.softaai.mvvmdemo.presentation.ui.compose.MovieListScreen
import com.softaai.mvvmdemo.presentation.ui.theme.MVVMDemoTheme
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MVVMDemoTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    MovieListScreen()
                }
            }
        }
    }
}

Inside the onCreate method, the setContent method is used to set the main content of the activity. In this case, the content is the MovieListScreen composable function, which displays a list of movies.

Note — I have provided proper guidance on how to display the list of movies. Now, you can continue building the movie details screen by following similar patterns as discussed earlier. If you face any issues or need any further assistance, feel free to ask me.

Conclusion

In this article, we have demonstrated how to build an Android app using Clean Architecture, MVVM, Kotlin, Room, Hilt, Retrofit, Moshi, Flow, and Jetpack Compose. We have covered all aspects of the app development process, including defining the data model, implementing the repository layer, defining the ViewModel, and defining the UI. By following these best practices, we can create robust and maintainable Android apps that are easy to test and evolve over time.

Note — I have provided proper guidance on how to display the list of movies. I now expect you to complete the remaining work by following the guidelines.

TCA

Architecting Success: A Comprehensive Guide to TCA Architecture in Android Development

I had the opportunity to work with the TCA (The Composable Architecture) in the past and would like to share my knowledge about it with our community. This architecture has gained popularity as a reliable way to create robust and scalable applications. TCA is a composable, unidirectional, and predictable architecture that helps developers to build applications that are easy to test, maintain and extend. In this blog, we’ll explore TCA in Android and how it can be implemented using Kotlin code.

What is TCA?

The Composable Architecture is a pattern that is inspired by Redux and Elm. It aims to simplify state management and provide a clear separation of concerns. TCA achieves this by having a strict unidirectional data flow and by breaking down the application into smaller, reusable components.

The basic components of TCA are:

  • State: The single source of truth for the application’s data.
  • Action: A description of an intent to change the state.
  • Reducer: A pure function that takes the current state and an action as input and returns a new state.
  • Effect: A description of a side-effect, such as fetching data from an API or showing a dialog box.
  • Environment: An object that contains dependencies that are needed to perform side-effects.

TCA uses a unidirectional data flow, meaning that the flow of data in the application goes in one direction. Actions are dispatched to the reducer, which updates the state, and effects are executed based on the updated state. This unidirectional flow makes the architecture predictable and easy to reason about.

Implementing TCA in Android using Kotlin

To implement TCA in Android using Kotlin, we will use the following libraries:

  • Kotlin Coroutines: For handling asynchronous tasks.
  • Kotlin Flow: For creating reactive streams of data.
  • Compose: For building the UI.

Let’s start by creating a basic TCA structure for our application.

1. State

The State is the single source of truth for the application’s data. In this example, we will create a simple counter app, where the state will contain an integer value representing the current count.

Kotlin
data class CounterState(val count: Int = 0)

2. Action

Actions are descriptions of intents to change the state. In this example, we will define two actions, one to increment the count and another to decrement it.

Kotlin
sealed class CounterAction {
    object Increment : CounterAction()
    object Decrement : CounterAction()
}

3. Reducer

Reducers are pure functions that take the current state and an action as input and return a new state. In this example, we will create a reducer that updates the count based on the action.

Kotlin
fun counterReducer(state: CounterState, action: CounterAction): CounterState {
    return when (action) {
        is CounterAction.Increment -> state.copy(count = state.count + 1)
        is CounterAction.Decrement -> state.copy(count = state.count - 1)
    }
}

4. Effect

Effects are descriptions of side-effects, such as fetching data from an API or showing a dialog box. In this example, we don’t need any effects.

Kotlin
sealed class CounterEffect

5. Environment

The Environment is an object that contains dependencies that are needed to perform side-effects. In this example, we don’t need any dependencies.

Kotlin
class CounterEnvironment

6. Store

The Store is the central component of TCA. It contains the state, the reducer, the effect handler, and the environment. It also provides a way to dispatch actions and subscribe to state changes.

Kotlin
class CounterStore : CoroutineScope {
    private val job = Job()
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    private val _state = MutableStateFlow(CounterState())
    val state: StateFlow<CounterState> = _state.asStateFlow()

    fun dispatch(action: CounterAction) {
        val newState = counterReducer(_state.value, action)
        _state.value = newState
    }

    fun dispose() {
        job.cancel()
    }
}

We create a MutableStateFlow to hold the current state of the application. We also define a StateFlow to provide read-only access to the state. The dispatch function takes an action, passes it to the reducer, and updates the state accordingly. Finally, the dispose function cancels the job to clean up any ongoing coroutines when the store is no longer needed.

7. Compose UI

Now that we have our TCA components in place, we can create a simple UI to interact with the counter store. We will use Compose to create the UI, which allows us to define the layout and behavior of the UI using declarative code.

Kotlin
@Composable
fun CounterScreen(store: CounterStore) {
    val state = store.state.collectAsState()

    Column {
        Text(text = "Counter: ${state.value.count}")
        Row {
            Button(onClick = { store.dispatch(CounterAction.Increment) }) {
                Text(text = "+")
            }
            Button(onClick = { store.dispatch(CounterAction.Decrement) }) {
                Text(text = "-")
            }
        }
    }
}

We define a CounterScreen composable function that takes a CounterStore as a parameter. We use the collectAsState function to create a state holder for the current state of the store. Inside the Column, we display the current count and two buttons to increment and decrement the count. When a button is clicked, we dispatch the corresponding action to the store.

8. Putting it all together

To put everything together, we can create a simple MainActivity that creates a CounterStore and displays the CounterScreen.

Kotlin
class MainActivity : ComponentActivity() {
    private val store = CounterStore()

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

        setContent {
            CounterScreen(store)
        }
    }

    override fun onDestroy() {
        store.dispose()
        super.onDestroy()
    }
}

We create a CounterStore instance and pass it to the CounterScreen composable function in the setContent block. We also call disposeon the store when the activity is destroyed to clean up any ongoing coroutines.


Let’s take a look at a real-world example to gain a clearer understanding of this concept in action and see how it can be applied to solve practical problems.

Here’s another example of TCA in action using a Weather App an example.

1. State

Let’s start by defining the state of our weather app:

Kotlin
data class WeatherState(
    val location: String,
    val temperature: Double,
    val isFetching: Boolean,
    val error: String?
)

Our state consists of the current location, the temperature at that location, a flag indicating whether the app is currently fetching data, and an error message if an error occurs.

2. Action

Next, we define the actions that can be performed in our weather app:

Kotlin
sealed class WeatherAction {
    data class UpdateLocation(val location: String) : WeatherAction()
    object FetchData : WeatherAction()
    data class DataFetched(val temperature: Double) : WeatherAction()
    data class Error(val message: String) : WeatherAction()
}

We define four actions: UpdateLocation to update the current location, FetchData to fetch the weather data for the current location, DataFetched to update the temperature after the data has been fetched, and Error to handle errors that occur during the fetch.

3. Reducer

Our reducer takes the current state and an action and returns a new state based on the action:

Kotlin
fun weatherReducer(state: WeatherState, action: WeatherAction): WeatherState {
    return when (action) {
        is WeatherAction.UpdateLocation -> state.copy(location = action.location)
        WeatherAction.FetchData -> state.copy(isFetching = true, error = null)
        is WeatherAction.DataFetched -> state.copy(
            temperature = action.temperature,
            isFetching = false,
            error = null
        )
        is WeatherAction.Error -> state.copy(isFetching = false, error = action.message)
    }
}

Our reducer updates the state based on the action that is dispatched. We use the copy function to create a new state object with updated values.

4. Effects

Our effect is responsible for fetching the weather data for the current location:

Kotlin
fun fetchWeatherData(location: String): Flow<WeatherAction> = flow {
    try {
        val temperature = getTemperatureForLocation(location)
        emit(WeatherAction.DataFetched(temperature))
    } catch (e: Exception) {
        emit(WeatherAction.Error(e.message ?: "Unknown error"))
    }
}

Our effect uses a suspend function getTemperatureForLocation to fetch the weather data for the current location. We emit a DataFetched action if the data is fetched successfully and an Error action if an exception occurs.

5. Environment

Our environment provides dependencies required by our effect:

Kotlin
interface WeatherEnvironment {
    suspend fun getTemperatureForLocation(location: String): Double
}

class WeatherEnvironmentImpl : WeatherEnvironment {
    override suspend fun getTemperatureForLocation(location: String): Double {
        // implementation omitted
    }
}

Our environment defines a single function getTemperatureForLocation which is implemented by WeatherEnvironmentImpl.

Kotlin
import okhttp3.OkHttpClient
import okhttp3.Request

class WeatherEnvironmentImpl : WeatherEnvironment {
    
    private val client = OkHttpClient()
    
    override suspend fun getTemperatureForLocation(location: String): Double {
        val url = "https://api.openweathermap.org/data/2.5/weather?q=$location&appid=API_KEY&units=metric"
        val request = Request.Builder().url(url).build()

        val response = client.newCall(request).execute()
        val jsonResponse = response.body()?.string()
        val json = JSONObject(jsonResponse)
        val main = json.getJSONObject("main")
        return main.getDouble("temp")
    }
}

In this implementation, we’re using the OkHttpClient library to make an HTTP request to the OpenWeatherMap API. The API returns a JSON response, which we parse using the JSONObject class from the org.json package. We then extract the temperature from the JSON response and return it as a Double.

Note that the API_KEY placeholder in the URL should be replaced with a valid API key obtained from OpenWeatherMap.

6. Store

Our store holds the current state and provides functions to dispatch actions and read the current state:

Kotlin
class WeatherStore(environment: WeatherEnvironment) {
    private val _state = MutableStateFlow(WeatherState("", 0.0, false, null))
    val state: StateFlow<WeatherState> = _state.asStateFlow()

    private val job = Job()
    private val scope = CoroutineScope(job + Dispatchers.IO)

    fun dispatch(action: WeatherAction) {
        val newState = weatherReducer(_state.value, action)
        _state.value = newState

        when (action) {
            is WeatherAction.UpdateLocation -> {
                scope.launch {
                    val weatherAction = fetchWeatherData(newState.location).first()
                    dispatch(weatherAction)
                }
            }
            WeatherAction.FetchData -> {
                scope.launch {
                    val weatherAction = fetchWeatherData(newState.location).first()
                    dispatch(weatherAction)
                }
            }
            else -> Unit
        }
    }

    init {
        scope.launch {
            val weatherAction = fetchWeatherData(_state.value.location).first()
            dispatch(weatherAction)
        }
    }
}

Our store initializes the state with default values and provides a dispatch function to update the state based on actions. We use a CoroutineScope to run our effects and dispatch new actions as required.

In the init block, we fetch the weather data for the current location and dispatch a DataFetched action with the temperature.

In the dispatch function, we update the state based on the action and run our effect to fetch the weather data. If an UpdateLocation or FetchData action is dispatched, we launch a new coroutine to run our effect and dispatch a new action based on the result.

That’s a simple example of how TCA can be used in a real-world application. By using TCA, we can easily manage the state of our application and handle complex interactions between different components.


Testing in TCA

In TCA, the reducer is the most important component as it is responsible for managing the state of the application. Hence, unit testing the reducer is essential. However, there are other components in TCA such as actions, environment, and effects that can also be tested.

Actions can be tested to ensure that they are constructed correctly and have the intended behavior when dispatched to the reducer. Environment can be tested to ensure that it provides the necessary dependencies to the reducer and effects. Effects can also be tested to ensure that they produce the expected results when executed.

Unit Testing For Reducer

Kotlin
import kotlinx.coroutines.test.TestCoroutineDispatcher
import org.junit.Assert.assertEquals
import org.junit.Test

class WeatherReducerTest {

    private val testDispatcher = TestCoroutineDispatcher()

    @Test
    fun `update location action should update location in state`() {
        val initialState = WeatherState(location = "Satara")
        val expectedState = initialState.copy(location = "Pune")

        val actualState = weatherReducer(
            initialState,
            WeatherAction.UpdateLocation("Pune")
        )

        assertEquals(expectedState, actualState)
    }

    @Test
    fun `fetch data action should update fetching state and clear error`() {
        val initialState = WeatherState(isFetching = false, error = "Some error")
        val expectedState = initialState.copy(isFetching = true, error = null)

        val actualState = weatherReducer(initialState, WeatherAction.FetchData)

        assertEquals(expectedState, actualState)
    }

    @Test
    fun `data fetched action should update temperature and reset fetching state`() {
        val initialState = WeatherState(isFetching = true, temperature = 0.0)
        val expectedState = initialState.copy(isFetching = false, temperature = 20.0)

        val actualState = weatherReducer(
            initialState,
            WeatherAction.DataFetched(20.0)
        )

        assertEquals(expectedState, actualState)
    }

    @Test
    fun `error action should update error and reset fetching state`() {
        val initialState = WeatherState(isFetching = true, error = null)
        val expectedState = initialState.copy(isFetching = false, error = "Some error")

        val actualState = weatherReducer(
            initialState,
            WeatherAction.Error("Some error")
        )

        assertEquals(expectedState, actualState)
    }

    @Test
    fun `fetch data effect should emit data fetched action with temperature`() {
        val initialState = WeatherState(isFetching = false, temperature = 0.0)
        val expectedState = initialState.copy(isFetching = false, temperature = 20.0)

        val fetchTemperature = { 20.0 }

        val actualState = performActionWithEffect(
            initialState,
            WeatherAction.FetchData,
            fetchTemperature,
            testDispatcher
        )

        assertEquals(expectedState, actualState)
    }

    @Test
    fun `fetch data effect should emit error action with message`() {
        val initialState = WeatherState(isFetching = false, error = null)
        val expectedState = initialState.copy(isFetching = false, error = "Failed to fetch temperature")

        val fetchTemperature = { throw Exception("Failed to fetch temperature") }

        val actualState = performActionWithEffect(
            initialState,
            WeatherAction.FetchData,
            fetchTemperature,
            testDispatcher
        )

        assertEquals(expectedState, actualState)
    }
}

In this example, we test each case of the when expression in the weatherReducer function using different test cases. We also test the effect that is dispatched when the FetchData action is dispatched. We create an initial state and define an expected state after each action is dispatched or effect is performed. We then call the weatherReducer function with the initial state and action to obtain the updated state. Finally, we use assertEquals to compare the expected and actual states.

To test the effect, we define a function that returns a value or throws an exception, depending on the test case. We then call the performActionWithEffect function, passing in the initial state, action, and effect function, to obtain the updated state after the effect is performed. We then use assertEquals to compare the expected and actual states.

Furthermore, integration testing can also be performed to test the interactions between the components of the TCA architecture. For example, integration testing can be used to test the flow of data between the reducer, effects, and the environment.

Overall, while the reducer is the most important component in TCA, it is important to test all components to ensure the correctness and robustness of the application.


Advantages:

  1. Predictable state management: TCA provides a strict, unidirectional data flow, which makes it easy to reason about the state of your application. This helps reduce the possibility of unexpected bugs and makes it easier to maintain and refactor your codebase.
  2. Testability: The unidirectional data flow in TCA makes it easier to write tests for your application. You can test your reducers and effects independently, which can help you catch bugs earlier in the development process.
  3. Modularity: With TCA, your application is broken down into small, composable pieces that can be easily reused across your codebase. This makes it easier to maintain and refactor your codebase as your application grows.
  4. Error handling: TCA provides a clear path for error handling, which makes it easier to handle exceptions and recover from errors in your application.

Disadvantages:

  1. Learning curve: TCA has a steep learning curve, especially for developers who are new to functional programming. You may need to invest some time to learn the concepts and get comfortable with the syntax.
  2. Overhead: TCA can introduce some overhead, especially if you have a small application. The additional boilerplate code required to implement TCA can be a barrier to entry for some developers.
  3. More verbose code: The strict, unidirectional data flow of TCA can lead to more verbose code, especially for more complex applications. This can make it harder to read and maintain your codebase.
  4. Limited tooling: TCA is a relatively new architecture, so there is limited tooling and support available compared to more established architectures like MVP or MVVM. This can make it harder to find solutions to common problems or get help when you’re stuck.

Summary

In summary, each architecture has its own strengths and weaknesses, and the best architecture for your project depends on your specific needs and requirements. TCA can be a good choice for projects that require predictable state management, testability, and modularity, but it may not be the best fit for every project.

new plugin convention

New Plugin Convention: Embracing the Positive Shift with Android Studio’s New Plugin Convention

Plugins are modules that provide additional functionality to the build system in Android Studio. They can help you perform tasks such as code analysis, testing, or building and deploying your app.

New Plugin Convention

The new plugin convention for Android Studio was introduced in version 3.0 of the Android Gradle plugin, which was released in 2017. While the new plugin convention was officially introduced in Android Studio 3.0, it is still being used and recommended in recent versions of Android Studio.

To define plugins in the build.gradle file, you can add the plugin’s ID to the plugins block.

Groovy
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    id 'com.android.application' version '7.4.2' apply false
    id 'com.android.library' version '7.4.2' apply false
    id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
}

In this example, the plugins block is used to define three different plugins: ‘com.android.application’, ‘com.android.library’, and ‘org.jetbrains.kotlin.android’. Each plugin is identified by its unique ID, and a specific version is specified as well. The ‘apply false’ statement means that the plugin is not applied to the current module yet — it will only be applied when explicitly called later on in the file.

Once you’ve defined your plugins in the build.gradle file, you can fetch them from the settings file. The settings file is typically located in the root directory of your project, and is named settings.gradle. You can add the following code to the settings.gradle file to fetch your plugins:

Groovy
pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}
rootProject.name = "AutoSilentDriveMvvm"
include ':app'

This is an example of the new convention for the settings.gradle file in Android Studio, which includes the pluginManagement and dependencyResolutionManagement blocks.

In the pluginManagement block, repositories are defined where Gradle can search for plugin versions. In this example, gradlePluginPortal(), google(), and mavenCentral() are included as repositories. These repositories provide access to a wide range of plugins that can be used in your Android project.

In the dependencyResolutionManagement block, repositories for dependency resolution are defined. The repositoriesMode is set to FAIL_ON_PROJECT_REPOS, which means that if a repository is defined in a module’s build.gradle file that conflicts with one of the repositories defined here, the build will fail. This helps to ensure that dependencies are resolved consistently across all modules in the project.

Finally, the rootProject.name and include statements are used to specify the name of the root project and the modules that are included in the project. In this example, there is only one module, :app, but you can include multiple modules by adding additional include statements.

Advantages Over Traditional Way

The new convention of defining plugins in the build.gradle file and fetching them from the settings file in Android Studio was introduced to improve the modularity and maintainability of the build system.

Traditionally, plugins were defined in a separate file called “buildscript.gradle” and fetched from a separate “build.gradle” file. This approach made it difficult to manage and update plugins, especially in large projects with many dependencies.

By defining plugins in the build.gradle file, the build system becomes more modular and easier to maintain. Each module can specify its own set of plugins, and the build system can handle transitive dependencies automatically.

Fetching plugins from the settings file also provides a central location for managing and updating plugin versions. This approach ensures that all modules use the same version of a plugin, which helps to avoid conflicts and makes it easier to upgrade to newer versions of a plugin.

Disadvantages

  1. Complexity: The new convention adds some complexity to the build system, especially for developers who are not familiar with Gradle or Android Studio. This complexity can make it harder to understand and troubleshoot issues that arise during the build process.
  2. Learning Curve: The new convention requires developers to learn a new way of managing plugins, which can take time and effort. Developers who are used to the traditional approach may find it challenging to adapt to the new convention.
  3. Migration: Migrating an existing project from the traditional approach to the new convention can be time-consuming and error-prone. Developers may need to update multiple files and dependencies, which can introduce new issues and require extensive testing.
Android Product Flavors and Build Variants

A Deep Dive into Android Product Flavors and Build Variants for Enhanced App Development

In Android development, product flavors allow you to create different versions of your app with different configurations, resources, and code, but with the same base functionality. Product flavors are used when you want to create multiple versions of the same app that differ in some way, such as a free and a paid version, or versions for different countries or languages.

For example, suppose you are creating a language-learning app that supports multiple languages, such as English, Spanish, and French. You could create three different product flavors, one for each language, and configure each flavor to include the appropriate language resources, such as strings, images, and audio files. Each flavor would share the same core codebase but would have different resources and configurations (or consider ABC 123 Learn and Akshar Learn apps, for which I am handling these use cases).

Android Product Flavors

Product flavors are defined in the build.gradle file of your app module. You can define the configuration for each flavor, including things like applicationId, versionName, versionCode, and buildConfigField values. You can also specify which source sets to include for each flavor, which allows you to create different versions of the app with different code, resources, or assets.

Build variants, on the other hand, are different versions of your app that are created by combining one or more product flavors with a build type. Build types are used to specify the build configuration, such as whether to enable debugging or optimize for size, while product flavors are used to specify the app’s functionality and resources.

For example, if you have two product flavors, “free” and “paid”, and two build types, “debug” and “release”, you would have four different build variants: “freeDebug”, “freeRelease”, “paidDebug”, and “paidRelease”. Each build variant would have its own configuration, resources, and code, and could be signed with a different key or configured for different deployment channels.

Resource Merging & Flavor Dimensions

Resource merging is an important part of using product flavors in Android development. When you have multiple product flavors with their own resources, such as layouts, strings, and images, the Android build system needs to merge them together to create the final APK.

Here’s an example of how resource merging works with product flavors:

Kotlin
android {
    // Define the flavor dimensions
    flavorDimensions "language", "version"
    
    // Define the product flavors
    productFlavors {
        englishFree {
            dimension "language"
            applicationId "com.softaai.app.en"
            resValue "string", "app_name", "My App (English)"
        }
        spanishFree {
            dimension "language"
            applicationId "com.softaai.app.es"
            resValue "string", "app_name", "Mi Aplicación (Español)"
        }
        pro {
            dimension "version"
            applicationId "com.softaai.app.pro"
            resValue "string", "app_name", "My App Pro"
        }
        free {
            dimension "version"
            applicationId "com.softaai.app.free"
            resValue "string", "app_name", "My App Free"
        }
    }
}

In this example, we have two flavor dimensions, “language” and “version”. We define four product flavors, “englishFree”, “spanishFree”, “pro”, and “free”, each with their own application ID and app name.

Well, What is Flavor Dimension?

Flavor dimension is a concept in Android Gradle build system that allows the grouping of related product flavors. It is used when an app has multiple sets of product flavors that need to be combined together. For example, if an app is available in multiple countries and each country has multiple build types, then we can use flavor dimensions to group the country-specific flavors together

When we build the app, the Android build system will merge the resources for each product flavor into the final APK. For example, if we have a layout file called “activity_main.xml” in the “res/layout” folder for both “englishFree” and “spanishFree”, the build system will merge them into a single “activity_main.xml” file that includes the appropriate resources for each language.

Now, let’s take a look at how we can use flavor dimensions to create more complex combinations of product flavors:

Kotlin
android {
    // Define the flavor dimensions
    flavorDimensions "language", "version"
    
    // Define the product flavors
    productFlavors {
        en {
            dimension "language"
            applicationId "com.softaai.app.en"
            resValue "string", "app_name", "My App (English)"
        }
        es {
            dimension "language"
            applicationId "com.softaai.app.es"
            resValue "string", "app_name", "Mi Aplicación (Español)"
        }
        pro {
            dimension "version"
            applicationId "com.softaai.app.pro"
            resValue "string", "app_name", "My App Pro"
        }
        free {
            dimension "version"
            applicationId "com.softaai.app.free"
            resValue "string", "app_name", "My App Free"
        }
        enPro {
            dimension "language"
            dimension "version"
            applicationId "com.softaai.app.enpro"
            resValue "string", "app_name", "My App Pro (English)"
        }
        esPro {
            dimension "language"
            dimension "version"
            applicationId "com.softaai.app.espro"
            resValue "string", "app_name", "Mi Aplicación Pro (Español)"
        }
    }
}

In this example, we have two flavor dimensions, “language” and “version”, and six product flavors. The “en” and “es” flavors represent the English and Spanish versions of the app, while the “pro” and “free” flavors represent the paid and free versions of the app. We also define two additional product flavors, “enPro” and “esPro”, which combine both language and version dimensions.

When we build the app, the Android build system will merge the resources for each product flavor into the final APK. For example, if we have a layout file called “activity_main.xml” in the “res/layout” folder for both “en” and “es” flavors, the build system will merge them into a single “activity_main.xml” file that includes the appropriate resources for each language. Similarly, if we have a string resource called “app_name” in the “pro” and “en” flavors, the build system will merge them into a single “app_name” resource that includes the appropriate version and language.

We can also use flavor dimensions to create more complex combinations of product flavors. In the example above, we define two additional product flavors, “enPro” and “esPro”, which combine both language and version dimensions. This means that we can create four different versions of the app: “enFree”, “esFree”, “enPro”, and “esPro”, each with their own application ID and app name.

Here’s an example of how we can reference resources for different flavor dimensions in our code:

Kotlin
// Get the app name for the current flavor
String appName = getResources().getString(R.string.app_name);

// Get the app name for the "enPro" flavor
String enProAppName = getResources().getString(R.string.app_name, "en", "pro");

// Get the app name for the "esFree" flavor
String esFreeAppName = getResources().getString(R.string.app_name, "es", "free");

In this example, we use the getResources() method to get a reference to the app’s resources. We then use the getString() method to get the app name for the current flavor, as well as for the “enPro” and “esFree” flavors, which have different values for the “language” and “version” dimensions.

Pre-Variant Dependencies

When we use product flavors and flavor dimensions to create different variants of our app, we may also need to use different dependencies for each variant. This is where pre-variant dependencies come into play.

Pre-variant dependencies are dependencies that are defined outside of the product flavors and flavor dimensions. These dependencies are applied to all variants of the app, regardless of the product flavor or flavor dimension. We can define pre-variant dependencies in the build.gradle file, outside of the productFlavors and flavorDimensions blocks.

Here’s an example of how we can define pre-variant dependencies:

Kotlin
dependencies {
    // Pre-variant dependencies
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'

    // Flavor-specific dependencies
    flavorDimensions 'version', 'language'
    productFlavors {
        pro {
            dimension 'version'
        }
        free {
            dimension 'version'
        }
        en {
            dimension 'language'
        }
        es {
            dimension 'language'
        }
    }

    // Dependencies for specific flavor dimensions
    enImplementation 'com.squareup.retrofit2:retrofit:2.9.0'
    esImplementation 'com.squareup.okhttp3:okhttp:4.9.3'
    proImplementation 'com.google.firebase:firebase-crashlytics:18.4.1'
}

In this example, we define two pre-variant dependencies: com.google.android.material:material:1.5.0 and androidx.appcompat:appcompat:1.4.1. These dependencies will be applied to all variants of the app, regardless of the product flavor or flavor dimension.

We then define four product flavors, two for the “version” dimension and two for the “language” dimension. We also define flavor-specific dependencies for each flavor dimension. For example, we define enImplementation 'com.squareup.retrofit2:retrofit:2.9.0' for the “en” flavor, which means that this dependency will only be applied to variants that include the “en” flavor.

Finally, we define a pro-variant dependency using the proImplementation keyword. This dependency will be applied only to variants that include the “pro” flavor.

Summary

Overall, product flavors and build variants provide a powerful and flexible way to create different versions of our Android app for different use cases. By combining different flavors and types, we can create highly customizable builds that meet the specific needs of our users.

Android Studio and Gradle

Android Studio and Gradle: The Dynamic Duo of Android Development

Android Studio and Gradle are two essential tools for developing Android applications. Android Studio is the official integrated development environment (IDE) for Android, while Gradle is the build system used to compile and package your code into an Android application package (APK) file.

Android Studio has an editor with sophisticated code completion and static analysis features. It also has a suite of tools for integrating with Android devices and emulators. The one thing it doesn’t have, however, is an integrated build system. Android Studio delegates the entire build process to Gradle. That’s everything that happens to turn your sources and resources into an APK that you can install on your device.

In this blog post, we’ll explore the features and benefits of Android Studio and Gradle, and how they work together to streamline the Android development process.

Android Studio and Gradle

Android Studio

Android Studio is a powerful and feature-rich IDE that provides developers with a comprehensive set of tools for designing, building, and testing Android applications. Android Studio is built on top of the IntelliJ IDEA platform, which provides a rich and flexible environment for Java and Kotlin development. Some of the key features of Android Studio include:

  1. Layout editor: Android Studio includes a powerful layout editor that allows you to design and preview your app’s user interface (UI) using a drag-and-drop interface. The layout editor also supports a variety of UI components and layouts, making it easy to create a visually appealing and functional UI.
  2. Code editor: Android Studio provides a code editor that supports syntax highlighting, code completion, and code navigation. The code editor also supports a variety of keyboard shortcuts and other productivity features that make coding faster and more efficient.
  3. Debugging tools: Android Studio includes a range of debugging tools that help you identify and fix bugs in your code. The debugger allows you to set breakpoints, inspect variables, and step through your code line by line.
  4. Emulator: Android Studio includes an emulator that allows you to test your app on a variety of virtual devices, including different screen sizes, resolutions, and API levels. The emulator also supports hardware acceleration, making it faster and more responsive than traditional emulators.

Gradle

Gradle is the build system used to compile and package your code into an APK file that can be installed on Android devices. Gradle is built on top of the Groovy programming language and provides a flexible and extensible build system that supports a variety of build configurations and dependencies. Some of the key features of Gradle include:

  1. Build configurations: Gradle allows you to define multiple build configurations for your app, such as “debug” and “release”. Each build configuration can have its own set of build settings and dependencies, making it easy to customize your app for different environments.
  2. Dependency management: Gradle provides a powerful dependency management system that allows you to declare dependencies on other libraries and modules. Gradle automatically downloads and configures the required dependencies, making it easy to include third-party libraries in your app.
  3. Incremental builds: Gradle supports incremental builds, which means that only the parts of your code that have changed since the last build are recompiled. This makes the build process faster and more efficient, especially for large codebases.
  4. Plugins: Gradle supports a variety of plugins that extend its functionality and make it easier to perform common tasks, such as building and testing your app. There are also third-party plugins available for Gradle that provide additional features and integration with other tools.

How Android Studio and Gradle Work Together?

Android Studio and Gradle work together to provide a seamless development experience for Android developers. Android Studio includes built-in support for Gradle, which means that you can easily create and manage Gradle projects within the IDE. When you create a new Android project in Android Studio, the IDE generates a basic Gradle build file that you can customize to suit your project’s needs.

The build file defines the build configurations, dependencies, and other settings for your project. When you build your project, Gradle reads the build file and compiles your code into an APK file. Android Studio provides a graphical interface for managing your Gradle build file, making it easy to configure and customize your build settings.

Android Studio and Gradle also provide a range of plugins and extensions that help you streamline your development workflow. For example, the Android Gradle plugin provides additional functionality for building and testing Android applications, such as support for the Android SDK and integration with the Google Play Store.

Conclusion

Android Studio and Gradle are essential tools for developing high-quality Android applications. Android Studio provides a powerful IDE with a range of features and tools for designing, building, and testing Android apps. Gradle provides a flexible and extensible build system that allows you to customize your app for different environments and include third-party libraries and dependencies. Together, Android Studio and Gradle provide a seamless and efficient development experience for Android developers.

R8

A Deep Dive into ProGuard, R8, and Reverse Engineering Protection

In the ever-evolving landscape of mobile app development, security remains a top concern for developers. Android, being one of the most popular mobile operating systems, is a prime target for malicious actors seeking to exploit vulnerabilities and reverse engineer applications for unauthorized access to sensitive data. To counter these threats, developers employ various techniques, with ProGuard and R8 playing pivotal roles in enhancing the security of Android applications.

Understanding ProGuard

ProGuard is a tool used in Android development to optimize and obfuscate code. It’s an optimization tool that can remove unused code and shrink the size of the application. It also helps to make the code difficult to understand or reverse-engineer by renaming classes, methods, and fields, which is called obfuscation.

ProGuard comes with a set of default rules that are applied to the code during the build process. However, developers can also define their own rules for specific classes, methods, or fields. The rules are defined in a ProGuard configuration file, usually named proguard-rules.pro, which is located in the app module of the Android project.

Here are some examples of ProGuard rules:

  1. Keep a specific class:
Plaintext
-keep class com.example.MyClass { *; }

This rule ensures that the class com.example.MyClass is not removed during the optimization process.

2. Keep a specific method:

Plaintext
-keepclassmembers class com.example.MyClass {
    public void myMethod(java.lang.String);
}

This rule ensures that the method myMethod in the class com.example.MyClass is not removed during the optimization process.

3. Obfuscate class and method names:

Plaintext
-keepnames class com.example.MyClass {
    void myMethod(java.lang.String);
}

This rule obfuscates the names of the class com.example.MyClass and the method myMethod.

4. Remove unused classes:

Plaintext
-dontwarn com.example.UnusedClass
-keep class com.example.** { *; }
-dontnote com.example.UnusedClass

This rule removes the unused class com.example.UnusedClass from the application and keeps all classes in the com.example package.

These are just a few examples of ProGuard rules that can be used in Android development. ProGuard is a powerful tool that can help optimize and secure an Android application, but it requires careful configuration to avoid unintended consequences.

Does ProGuard provide 100% protection against reverse engineering?

ProGuard is a useful tool for making reverse engineering of Android applications more difficult, but it does not provide 100% protection against reverse engineering.

While ProGuard can obfuscate the code, it does not encrypt it. This means that a determined attacker could still decompile and reverse engineer the code with enough time and effort. Additionally, ProGuard cannot protect against other methods of reverse engineering, such as debugging or analyzing network traffic.

It’s important to note that while ProGuard can make reverse engineering more difficult, it’s not a substitute for implementing proper security measures in an Android application. Developers should also consider other security techniques, such as encryption and secure coding practices, to help protect against reverse engineering and other types of attacks.

What about R8?

R8 is another code shrinking and obfuscation tool that is used in Android development, similar to ProGuard. R8 is included in the Android Gradle plugin and can be enabled by adding the following line to the app module’s build.gradle file:

Groovy
android {
    ...
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

When R8 is enabled, it analyzes the code and removes unused code and resources, as well as obfuscates the code to make it more difficult to understand or reverse engineer. R8 can achieve similar results to ProGuard, but with better performance and more reliable mapping files for debugging.

One benefit of using R8 is that it is faster than ProGuard, which can result in faster build times. Additionally, R8 can also remove dead code more aggressively than ProGuard, resulting in smaller APK file sizes.

Like ProGuard, R8 is not a foolproof solution for protecting an Android application from reverse engineering, but it can help make it more difficult for attackers to understand and modify the code

Protecting Android App from Reverse Engineering: Best Practices

It’s important to note that there is no 100% reverse engineering safe solution for any software, including Android applications. However, there are several measures that can be taken to make reverse engineering more difficult and protect sensitive data.

  1. Code Obfuscation: Obfuscation is the process of modifying code to make it difficult to understand or reverse engineer. This can be done using tools like ProGuard, which can rename classes, methods, and fields to make them more difficult to understand.
  2. Encryption: Encrypting sensitive data can make it more difficult for attackers to extract information from the application. This can be done using encryption libraries or by implementing secure communication protocols.
  3. Tamper Detection: Implementing tamper detection mechanisms can help detect if the application has been modified or tampered with. This can be done by implementing checksums or digital signatures that can be checked during runtime.
  4. Anti-Debugging Techniques: Implementing anti-debugging techniques can make it more difficult for attackers to debug the application and extract sensitive data. This can be done by implementing code that detects if the application is running in a debugging environment and terminates if it is.
  5. Secure Coding Practices: Following secure coding practices can help prevent vulnerabilities in the application that can be exploited by attackers. This includes practices like input validation, error handling, and secure data storage.

By implementing a combination of these measures, it is possible to make reverse engineering of an Android application more difficult and protect sensitive data. However, it’s important to note that no solution can provide 100% protection against reverse engineering, and implementing security measures is an ongoing process that requires constant monitoring and updating to stay ahead of attackers.

Conclusion

In the constant cat-and-mouse game between developers and malicious actors, the use of tools like ProGuard and R8 is essential for fortifying Android applications against reverse engineering and unauthorized access. By leveraging code obfuscation, optimization, and additional protective measures, developers can significantly enhance the security posture of their apps. Continuous vigilance, staying informed about emerging threats, and adopting best practices in secure coding remain key components of a robust mobile app security strategy.

Image vs Vector Assets in Android

Mastering Image vs Vector Assets in Android: A Comprehensive Guide for Power-Packed Development

Image Vs Vector assets

When it comes to creating visually appealing and responsive Android applications, developers often grapple with the decision of using image or vector assets. Both play crucial roles in the overall user experience, but understanding the differences between the two and when to use each is essential. In this comprehensive guide, we’ll explore the characteristics, advantages, and use cases of image and vector assets in the context of Android development.

Understanding Image vs Vector Assets in Android

In Android Studio, image assets and vector assets are two types of resources that can be used in an Android application.

Image assets:

Image assets are raster images that are designed using a bitmap format such as JPEG, PNG, and GIF. These are pixel-based graphics that are made up of a grid of pixels. Image assets are great for displaying realistic images or photographs in your app. However, when it comes to resizing, they can lose quality and become pixelated.

In Android Studio, you can create image assets by going to the “res” folder and selecting “New > Image Asset”. You can then select the image you want to use and choose various settings such as the file type, size, and name.

Vector assets:

Vector assets are graphics that are created using mathematical equations to define lines, curves, and shapes. Vector assets are resolution-independent and can be scaled to any size without losing quality. They are great for displaying icons, logos, and other graphics that need to be displayed in multiple sizes and resolutions.

In Android Studio, you can create vector assets by going to the “res” folder and selecting “New > Vector Asset”. You can then select the image you want to use and choose various settings such as the name, color, and size.

Image Asset Vs Vector Asset :

The main difference between image assets and vector assets in Android Studio is that image assets are raster images, while vector assets are made up of mathematical equations.

Here are some of the key differences between image assets and vector assets:

  1. Resolution: Image assets are made up of a fixed number of pixels and have a fixed resolution, while vector assets are resolution-independent and can be scaled up or down without losing quality.
  2. Size: Image assets can be very large in size, especially if they are high-resolution, while vector assets are generally much smaller in size.
  3. Quality: When you resize an image asset, it can become blurry or pixelated, while vector assets maintain their quality at any size.
  4. Compatibility: Image assets may not be compatible with all devices or screen resolutions, while vector assets can be used on any device and screen resolution.
  5. Editing: Image assets can be edited using image editing software like Photoshop, while vector assets can be edited using vector graphics software like Adobe Illustrator.

In general, if you need to display a realistic image or photograph in your app, use image assets. If you need to display an icon or logo that needs to be displayed in multiple sizes and resolutions, use vector assets.

Choosing the Right Asset for the Right Scenario

  1. Performance Considerations:
    • Image assets may be preferable for static, high-detail visuals.
    • Vector assets are optimal for scalable elements, icons, and animations.
  2. App Size and Loading Times:
    • Image assets contribute to larger app sizes.
    • Vector assets help reduce app size and loading times.
  3. Screen Density:
    • Image assets require multiple versions for different screen densities.
    • Vector assets scale seamlessly across various screen densities.
  4. Editing and Customization:
    • Image assets are often more challenging to edit without losing quality.
    • Vector assets can be easily customized and modified without compromising quality.
  5. Dynamic UI Elements:
    • For dynamic and interactive UI elements, vectors are preferred.
    • Image assets may be suitable for static elements with no need for scalability.

Summary

In the dynamic world of Android development, the choice between image and vector assets is a critical consideration. Both have their strengths and weaknesses, and the decision should be based on the specific requirements of the app. Striking a balance between visual appeal, performance, and scalability is key to creating a successful and engaging Android application. By understanding the characteristics of each asset type, developers can make informed choices that contribute to a seamless and visually pleasing user experience.

In summary, image assets are raster graphics that are great for displaying realistic images, while vector assets are resolution-independent and are great for displaying icons, logos, and other graphics that need to be displayed in multiple sizes and resolutions.

Simplifying Android In-App Billing with Google-IAP Library (Play Billing Library Version 5.0.0)

Google-IAP Library
Android In-App Billing / Google-IAP Library

In the dynamic realm of Android app development, the process of implementing In-App Billing can be both challenging and time-consuming. To ease the burden on budding Android developers, today I am excited to share an easy-to-implement solution: the Google-IAP library, specifically tailored for In-App Billing. This library not only streamlines the implementation process but also minimizes the code required for handling in-app purchases.

Overview of Android In-App Billing / Google-IAP Library

The Google-IAP library is designed to simplify the integration of In-App Billing into Android applications. It stands out for its minimalist approach, offering developers a quick and efficient solution for testing and fast-paced development. With minimal lines of code, the library enables seamless in-app purchase handling, making it an ideal choice for novice Android developers.

Advantages of Google-IAP Library

  1. Minimal Code: One of the standout features of the Google-IAP library is its minimalistic approach to code. Developers can achieve in-app billing functionality with just a few lines of code, reducing the complexity and making the integration process more accessible for beginners.
  2. Fast Development: Time is of the essence in the world of app development, and the Google-IAP library acknowledges this reality. By providing a straightforward and efficient solution, it enables developers to implement in-app purchases rapidly, accelerating the overall development process.
  3. Ease of Testing: The library comes with built-in features that facilitate testing. This is especially beneficial during the development phase, allowing developers to ensure that the in-app billing functionality works as expected without the need for extensive debugging.

The Importance of Understanding Google Play Billing Library

While the Google-IAP library offers a convenient solution for quick implementation, it is crucial to emphasize the significance of understanding and considering the official Google Play Billing Library. Google Play Billing Library is a Google product, ensuring continuous updates and support, and is the recommended tool for handling in-app purchases on the Android platform.

Recommendation for Developers

As a best practice, it is strongly recommended that developers first gain a thorough understanding of the Google Play Billing Library and attempt to use it in their projects. Google’s product comes with the assurance of ongoing updates and support, ensuring compatibility with the latest Android versions and addressing any potential issues.

Caution Regarding Third-Party Libraries: While third-party libraries like Google-IAP can offer quick solutions, there is always a level of uncertainty regarding future updates and support. Relying solely on third-party libraries may lead to complications if they are not actively maintained. To avoid potential consequences in the long run, it is advisable to prioritize the official Google Play Billing Library for in-app billing implementations.

Conclusion

In conclusion, the Google-IAP (Play Billing Library Version 5.0.0) emerges as a handy tool for Android developers, especially those looking for a quick and easy solution for in-app billing. However, it is imperative to balance expediency with long-term stability. Developers are encouraged to first understand and consider the Google Play Billing Library, harnessing the power of an official Google product for robust and future-proof in-app purchase implementations. By doing so, developers can strike a balance between speed and reliability in their Android app development journey.

error: Content is protected !!