π± Android App Project with MVVM, Dagger Hilt, and Room Database
This Android project is built using modern Android development practices, leveraging the MVVM (Model-View-ViewModel) architectural pattern. It incorporates Dagger Hilt for efficient and scalable dependency injection and Room Database for local data storage and persistence.
π§ Key Technologies Used:
MVVM Architecture: Ensures a clean separation of concerns, making the codebase more modular, testable, and maintainable.
Dagger Hilt: Simplifies dependency injection in Android by providing a standard way to manage app-level dependencies.
Room Database: Provides an abstraction layer over SQLite to allow robust database access while maintaining compile-time verification of SQL queries.
β
Project Goals:
Implement a clean and scalable architecture for Android applications.
Use dependency injection to reduce boilerplate code and improve testability.
Store and manage structured local data using Room with LiveData and Coroutines support.
This project serves as a foundation for building efficient, scalable, and maintainable Android applications using best practices and modern tools recommended by Google.
Gradle Dependencies
//UI size
implementation("com.intuit.sdp:sdp-android:1.1.1")
//API Calling(Retrofit)
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
//Dagger Hilt
implementation("com.google.dagger:hilt-android:2.51.1")
kapt("com.google.dagger:hilt-android-compiler:2.51.1")
//lifeCycleScopes
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
//for viewModels()
implementation("androidx.navigation:navigation-fragment-ktx:2.5.3")
//Glide
implementation("com.github.bumptech.glide:glide:4.16.0")
//Room Database
val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
implementation("androidx.room:room-ktx:$room_version")
kapt("androidx.room:room-compiler:$room_version")
// Allow references to generated code
kapt {
correctErrorTypes = true
}
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("com.google.dagger.hilt.android")
id("kotlin-android")
}
id("com.google.dagger.hilt.android") version "2.51.1" apply false
Create Application class with @HiltAndroid App Annotation
@HiltAndroidApp
class ApplicationClass : Application() {
}
Create AppModule for Singleton Objects
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
@Provides
fun provideApiClient(): Retrofit {
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
val okHttpClient: OkHttpClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.addInterceptor(Interceptor { chain ->
val request = chain.request().newBuilder()
request.header("Content-Type", "application/json")
request.header(
"Authorization",
"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjozMDgsIm1vYmlsZV9ubyI6Ijk2NjI0NDU1NzMiLCJlbWFpbF9pZCI6InRlc3R3b3MwQGdtYWlsLmNvbSIsImNvdW50cnlfaWQiOjEsInJvbGVfbmFtZSI6ImJ1eWVyIiwic2VsbGVyX2lkIjoxODUsImJ1eWVyX2lkIjoxNTgsIm5hbWUiOiJLZWxzZXkgSmFja3Nvbm4iLCJ0eXBlIjoibG9naW4iLCJpYXQiOjE3NDMxMzY3MTAsImV4cCI6MTc3NDY3MjcxMH0.z8vmQiXUupZvyu-QM0vWCbsCS7846fBBeWRgYFUZVDU"
)
chain.proceed(request.build())
}).build()
return Retrofit.Builder()
.baseUrl(Constance.BASEURL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
@Provides
fun provideDatabase(@ApplicationContext context: Context): MyDatabase {
return Room.databaseBuilder(context, MyDatabase::class.java, Constance.DBNAME).build()
}
@Provides
fun provideContext(@ApplicationContext context: Context): Context {
return context
}
}
Create View Models with hiltViewModel
@HiltViewModel
class HomeViewModel @Inject constructor(
private val context: Application,
private val repository: HomeRepository,
private val myDatabase: MyDatabase
) : AndroidViewModel(context) {
private var _dishesList: MutableLiveData<Resource<List<GetDishes.ResponseData.Dish>>> =
MutableLiveData()
val dishesList: LiveData<Resource<List<GetDishes.ResponseData.Dish>>> = _dishesList
fun fetchDishes(hashMap: HashMap<String, Any>) {
viewModelScope.launch(Dispatchers.IO) {
_dishesList.postValue(Resource.Loading())
repository.getAllDishes(hashMap).data?.responseData?.dishes?.let {
_dishesList.postValue(Resource.Success(it))
myDatabase.dishDao().insertDishes(it)
}
}
}
}
Create a Repository for Data Fetch to the server
class HomeRepository @Inject constructor(private val apiService: ApiService) {
suspend fun getAllDishes(dishDetails: HashMap<String, Any>): Resource<GetDishes> {
return try {
val response = apiService.getDishes(dishDetails)
if (response.isSuccessful && response.body() != null) {
Resource.Success(response.body()!!)
} else {
Resource.Error("${Constance.APIERROR} ${response.code()}")
}
} catch (e: Exception) {
Resource.Error("${Constance.APIERROR} ${e.localizedMessage}")
}
}
}
Create Api Interface For api calling
interface ApiService {
@POST("get-dishes")
suspend fun getDishes(@Body dishObject: HashMap<String, Any>): Response<GetDishes>
}
Create Room Database ( DAO, Database, DataModelclass & TypeConverter
@Dao
interface DishesDAO {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertDishes(dishesData: List<GetDishes.ResponseData.Dish>)
@Query("UPDATE Dishes SET isFavorite=:isFavorite WHERE itemId=:id")
suspend fun updateDish(id: Int, isFavorite: Boolean)
@Query("SELECT * FROM dishes")
fun getAllDishes(): LiveData<List<GetDishes.ResponseData.Dish>>
@Query("SELECT * FROM dishes")
suspend fun getAllDishesList(): List<GetDishes.ResponseData.Dish>
}
@Database(entities = [GetDishes.ResponseData.Dish::class], version = 6, exportSchema = false)
@TypeConverters(
FoodItemAvailableTimeTypeConverter::class,
CartTypeConverter::class,
FoodImageTypeConverter::class,
FoodItemVariantTypeConverter::class,
FoodSellerTypeConverter::class,
FoodSellerAvailableDayConverter::class
)
abstract class MyDatabase : RoomDatabase() {
abstract fun dishDao(): DishesDAO
}
class FoodItemAvailableTimeTypeConverter {
@TypeConverter
fun fromList(value: List<GetDishes.ResponseData.Dish.FoodItemAvailableTime>): String {
return Gson().toJson(value)
}
@TypeConverter
fun toList(value: String): List<GetDishes.ResponseData.Dish.FoodItemAvailableTime> {
val type =
object : TypeToken<List<GetDishes.ResponseData.Dish.FoodItemAvailableTime>>() {}.type
return Gson().fromJson(value, type)
}
}
class CartTypeConverter {
@TypeConverter
fun fromCart(value: GetDishes.ResponseData.Dish.Cart): String {
return Gson().toJson(value)
}
@TypeConverter
fun toCart(value: String): GetDishes.ResponseData.Dish.Cart {
return Gson().fromJson(value, GetDishes.ResponseData.Dish.Cart::class.java)
}
}
class FoodImageTypeConverter {
private val gson = Gson()
@TypeConverter
fun fromFoodImageList(value: List<GetDishes.ResponseData.Dish.FoodItemImage>?): String {
return gson.toJson(value) // Convert list to JSON
}
@TypeConverter
fun toFoodImageList(value: String?): List<GetDishes.ResponseData.Dish.FoodItemImage>? {
if (value == null) return emptyList()
val type = object : TypeToken<List<GetDishes.ResponseData.Dish.FoodItemImage>>() {}.type
return gson.fromJson(value, type) // Convert JSON back to list
}
}
class FoodItemVariantTypeConverter {
@TypeConverter
fun fromFoodItemVariantList(value: List<GetDishes.ResponseData.Dish.FoodItemVariant>): String {
return Gson().toJson(value)
}
@TypeConverter
fun toFoodItemVariantList(value: String): List<GetDishes.ResponseData.Dish.FoodItemVariant> {
val type = object : TypeToken<List<GetDishes.ResponseData.Dish.FoodItemVariant>>() {}.type
return Gson().fromJson(value, type)
}
}
class FoodSellerTypeConverter {
@TypeConverter
fun fromFoodSeller(value: GetDishes.ResponseData.Dish.FoodSeller): String {
return Gson().toJson(value)
}
@TypeConverter
fun toFoodSeller(value: String): GetDishes.ResponseData.Dish.FoodSeller {
return Gson().fromJson(value, GetDishes.ResponseData.Dish.FoodSeller::class.java)
}
}
class FoodSellerAvailableDayConverter {
@TypeConverter
fun fromFoodSellerAvailableDayList(value: List<GetDishes.ResponseData.Dish.FoodSeller.FoodSellerAvailableDay>): String {
return Gson().toJson(value)
}
@TypeConverter
fun toFoodSellerAvailableDayList(value: String): List<GetDishes.ResponseData.Dish.FoodSeller.FoodSellerAvailableDay> {
val type = object :
TypeToken<List<GetDishes.ResponseData.Dish.FoodSeller.FoodSellerAvailableDay>>() {}.type
return Gson().fromJson(value, type)
}
}
data class GetDishes(
@SerializedName("ResponseData")
@Expose
var responseData: ResponseData,
@SerializedName("ResponseStatus")
@Expose
var responseStatus: Int,
@SerializedName("ResponseText")
@Expose
var responseText: String,
@SerializedName("status")
@Expose
var status: Int,
@SerializedName("success")
@Expose
var success: Boolean
) {
data class ResponseData(
@SerializedName("count")
@Expose
var count: Int,
@SerializedName("dishes")
@Expose
var dishes: List<Dish>
) {
@Entity(tableName = "Dishes")
data class Dish(
@SerializedName("accept_advance_order")
@Expose
var acceptAdvanceOrder: Int? = 0,
@SerializedName("advance_order_time_span")
@Expose
var advanceOrderTimeSpan: String? = null,
@SerializedName("apply_gst")
@Expose
var applyGst: Int? = null,
@SerializedName("available_dishes")
@Expose
var availableDishes: Int? = null,
@SerializedName("cart")
@Expose
@TypeConverters(CartTypeConverter::class)
var cart: Cart? = null,
@SerializedName("category_id")
@Expose
var categoryId: Int? = null,
@SerializedName("created_date")
@Expose
var createdDate: String? = null,
@SerializedName("currency")
@Expose
var currency: String? = null,
@SerializedName("description")
@Expose
var description: String? = null,
/*@SerializedName("discount_expires_in")
@Expose
var discountExpiresIn: Any,*/
@SerializedName("discount_percent")
@Expose
var discountPercent: Int? = 0,
/*@SerializedName("discount_validity")
@Expose
var discountValidity: Any,*/
@SerializedName("dish_type")
@Expose
var dishType: String? = "",
@SerializedName("distance")
@Expose
var distance: Double? = 0.0,
@SerializedName("food_item_available_times")
@Expose
@TypeConverters(FoodItemAvailableTimeTypeConverter::class)
var foodItemAvailableTimes: List<FoodItemAvailableTime>? = null,
@SerializedName("food_item_images")
@Expose
@TypeConverters(FoodImageTypeConverter::class)
var foodItemImages: List<FoodItemImage>? = null,
@SerializedName("food_item_variants")
@Expose
@TypeConverters(FoodItemVariantTypeConverter::class)
var foodItemVariants: List<FoodItemVariant>? = null,
@SerializedName("food_seller")
@Expose
@TypeConverters(FoodSellerTypeConverter::class)
var foodSeller: FoodSeller? = null,
@SerializedName("gst")
@Expose
var gst: String? = "",
@SerializedName("gst_type")
@Expose
var gstType: String? = "",
@SerializedName("is_available")
@Expose
var isAvailable: Int? = 0,
@SerializedName("is_favorite")
@Expose
var isFavorite: Boolean? = false,
@SerializedName("is_recommended")
@Expose
var isRecommended: Int? = 0,
@SerializedName("item_id")
@Expose
@PrimaryKey(autoGenerate = false)
var itemId: Int = 0,
@SerializedName("item_name")
@Expose
var itemName: String? = "",
@SerializedName("make_to_order_time")
@Expose
var makeToOrderTime: String? = null,
/* @SerializedName("packing_charges")
@Expose
var packingCharges: Any,*/
@SerializedName("price")
@Expose
var price: Int? = null,
@SerializedName("rating")
@Expose
var rating: Double? = null,
@SerializedName("sameDayOrder")
@Expose
var sameDayOrder: Boolean? = null,
@SerializedName("seller_id")
@Expose
var sellerId: Int? = null,
@SerializedName("sold_dishes")
@Expose
@Nullable
var soldDishes: Int? = null,
@SerializedName("sub_category_id")
@Expose
var subCategoryId: Int? = null,
@SerializedName("total_reviews")
@Expose
var totalReviews: Int? = null,
@SerializedName("traveling_time")
@Expose
var travelingTime: Double? = 0.0,
@SerializedName("weight")
@Expose
var weight: String? = ""
) {
data class Cart(
@SerializedName("is_cart_item")
@Expose
var isCartItem: Boolean? = false,
@SerializedName("quantity")
@Expose
var quantity: Int? = 0
)
data class FoodItemAvailableTime(
@SerializedName("from_time")
@Expose
var fromTime: String? = "",
@SerializedName("time")
@Expose
var time: String? = "",
@SerializedName("to_time")
@Expose
var toTime: String? = ""
)
data class FoodItemImage(
@SerializedName("image_url")
@Expose
var imageUrl: String? = ""
)
data class FoodItemVariant(
@SerializedName("available_dishes")
@Expose
var availableDishes: Int? = 0,
/*@SerializedName("discount_expires_in")
@Expose
var discountExpiresIn: Any,*/
@SerializedName("discount_percent")
@Expose
var discountPercent: Int? = 0,
@SerializedName("item_id")
@Expose
var itemId: Int? = 0,
@SerializedName("max_dishes_per_day")
@Expose
var maxDishesPerDay: Int? = 0,
/*@SerializedName("packing_charges")
@Expose
var packingCharges: Any,*/
@SerializedName("price")
@Expose
var price: Int? = 0,
@SerializedName("variant_id")
@Expose
var variantId: Int? = 0,
@SerializedName("weight")
@Expose
var weight: String? = ""
)
data class FoodSeller(
@SerializedName("accept_orders")
@Expose
var acceptOrders: Int? = 0,
@SerializedName("business_name")
@Expose
var businessName: String? = "",
@SerializedName("food_seller_available_days")
@Expose
@TypeConverters(FoodSellerAvailableDay::class)
var foodSellerAvailableDays: List<FoodSellerAvailableDay>? = null,
@SerializedName("is_active")
@Expose
var isActive: Int? = 0,
@SerializedName("lattitude")
@Expose
var lattitude: String? = "",
@SerializedName("longitude")
@Expose
var longitude: String? = "",
@SerializedName("seller_id")
@Expose
var sellerId: Int? = 0,
@SerializedName("seller_type")
@Expose
var sellerType: String? = "",
@SerializedName("type_of_food")
@Expose
var typeOfFood: String? = "",
@SerializedName("user_id")
@Expose
var userId: Int? = 0
) {
data class FoodSellerAvailableDay(
@SerializedName("day_name")
@Expose
var dayName: String? = "",
@SerializedName("food_seller_available_times")
@Expose
var foodSellerAvailableTimes: List<FoodSellerAvailableTime>? = null,
@SerializedName("id")
@Expose
var id: Int? = 0,
@SerializedName("seller_id")
@Expose
var sellerId: Int? = 0,
@SerializedName("status")
@Expose
var status: Boolean? = false
) {
data class FoodSellerAvailableTime(
@SerializedName("day_id")
@Expose
var dayId: Int? = 0,
@SerializedName("from_time")
@Expose
var fromTime: String? = "",
@SerializedName("id")
@Expose
var id: Int? = 0,
@SerializedName("seller_id")
@Expose
var sellerId: Int? = 0,
@SerializedName("status")
@Expose
var status: Boolean? = false,
@SerializedName("to_time")
@Expose
var toTime: String? = ""
)
}
}
}
}
}
How to load Fragments: Hide Show
fun showFragment(fragment: Fragment, tag: String, shouldRefresh: Boolean = false): Boolean {
val transaction = fragmentManager.beginTransaction()
val existingFragment = fragmentManager.findFragmentByTag(tag)
if (existingFragment != null) {
if (shouldRefresh) {
existingFragment.arguments = fragment.arguments // Update arguments
if (existingFragment is Refreshable) {
existingFragment.refreshData() // Ensure data is refreshed
}
}
transaction.show(existingFragment)
fragmentManager.popBackStack(tag, 0) // Bring to top
} else {
transaction.add(binding.flContainer.id, fragment, tag)
// transaction.addToBackStack(tag)
}
fragmentManager.fragments.forEach {
if (it != existingFragment && it.isAdded) {
transaction.hide(it)
}
}
transaction.commitAllowingStateLoss()
return true
}
Android RecyclerView Pagination
class MainActivity : AppCompatActivity() {
private lateinit var adapter: ItemAdapter
private var isLoading = false
private var currentPage = 1
private val pageSize = 20
private var isLastPage = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
adapter = ItemAdapter(mutableListOf())
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
loadItems()
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(rv, dx, dy)
val layoutManager = rv.layoutManager as LinearLayoutManager
val totalItemCount = layoutManager.itemCount
val lastVisible = layoutManager.findLastVisibleItemPosition()
if (!isLoading && !isLastPage && lastVisible + 5 >= totalItemCount) {
loadItems()
}
}
})
}
private fun loadItems() {
isLoading = true
lifecycleScope.launch {
try {
val response = RetrofitClient.api.getItems(currentPage, pageSize)
if (response.isSuccessful) {
val itemResponse = response.body()
val items = itemResponse?.data ?: emptyList()
adapter.addItems(items)
currentPage++
isLastPage = items.size < pageSize
} else {
Log.e("API_ERROR", "Failed: ${response.message()}")
}
} catch (e: Exception) {
Log.e("API_ERROR", "Exception: ${e.message}")
} finally {
isLoading = false
}
}
}
}
π Final Words
With MVVM architecture, Dagger Hilt, and Room Database working together, this project is set up for performance, scalability, and maintainability. From clean code to powerful architecture, everything is in place.
Now itβs all ready to go β
This project is going to run like a charm! π₯
Letβs build something amazing. π
Top comments (0)