DEV Community

Devika Android
Devika Android

Posted on

Android MVVM, Dagger Hilt & Room Database with Local Storage

πŸ“± 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
Enter fullscreen mode Exit fullscreen mode

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
   }

}
Enter fullscreen mode Exit fullscreen mode

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)
           }
       }
   }

}
Enter fullscreen mode Exit fullscreen mode

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}")
       }
   }

}
Enter fullscreen mode Exit fullscreen mode

Create Api Interface For api calling

interface ApiService {

   @POST("get-dishes")
   suspend fun getDishes(@Body dishObject: HashMap<String, Any>): Response<GetDishes>

}
Enter fullscreen mode Exit fullscreen mode

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? = ""
                   )
               }
           }
       }
   }
}
Enter fullscreen mode Exit fullscreen mode

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
   }
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

πŸš€ 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)