<file_sep>package com.quypham.vdc.repo.weather

import com.quypham.vdc.api.RepoResponse
import com.quypham.vdc.data.WeatherForecastEntity

interface WeatherDataSource {
    suspend fun query(city: String, daysOfForecast: Int): RepoResponse<WeatherForecastEntity>
}<file_sep>package com.quypham.vdc.di

import android.content.Context
import androidx.room.Room
import com.quypham.vdc.Constants
import com.quypham.vdc.api.ApiService
import com.quypham.vdc.api.ServiceGenerator
import com.quypham.vdc.database.AppDatabase
import com.quypham.vdc.utils.NetworkUtil
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ApplicationComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.Interceptor
import javax.inject.Singleton


@InstallIn(ApplicationComponent::class)
@Module
class ApplicationModule {

    @Singleton
    @Provides
    fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(context, AppDatabase::class.java, Constants.APP_DATABASE_NAME)
            .fallbackToDestructiveMigration()
            .build()
    }

    @Singleton
    @Provides
    fun provideApiService(@ApplicationContext context: Context): ApiService {
        val cacheInterceptor = Interceptor { chain ->
            val requestBuilder = chain.request().newBuilder()
            if (NetworkUtil.hasNetworkConnection(context))
            //Get cache from last 5 mins in case internet is available
                requestBuilder.header("Cache-Control", "public, max-age=" + (5 * 60)).build()
            else
            //Get cache from 2 days in case no internet is available
                requestBuilder.header(
                    "Cache-Control",
                    "public, only-if-cached, max-stale=" + 60 * 60 * 24 * 7
                ).build()

            chain.proceed(requestBuilder.build())
        }

        ServiceGenerator.setCache(context.cacheDir)
        ServiceGenerator.setInterceptor(cacheInterceptor)
        ServiceGenerator.setBaseUrl(Constants.BASE_URL)
        return ServiceGenerator.createService(ApiService::class.java)
    }

}<file_sep>package com.quypham.vdc.base

import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import com.quypham.vdc.R

abstract class BaseActivity : AppCompatActivity() {
    protected fun addFragment(fragment: Fragment, tag: String) {
        addFragment(fragment, tag, R.id.container)
    }


    private fun addFragment(fragment: Fragment, tag: String?, frameId: Int) {
        supportFragmentManager.beginTransaction()
            .replace(frameId, fragment, tag)
            .addToBackStack(tag)
            .commitAllowingStateLoss()
    }
}<file_sep>package com.quypham.vdc.domain

import android.util.Log
import com.quypham.vdc.UseCase
import com.quypham.vdc.api.RepoResponse
import com.quypham.vdc.data.QueryForecastParameter
import com.quypham.vdc.data.WeatherForecastEntity
import com.quypham.vdc.di.IoDispatcher
import com.quypham.vdc.repo.WeatherRepository
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject

open class GetWeatherForecast @Inject constructor(private val mWeatherRepository: WeatherRepository,
                                                  @IoDispatcher dispatcher: CoroutineDispatcher) :
    UseCase<QueryForecastParameter, RepoResponse<WeatherForecastEntity>>(dispatcher) {
    companion object {
        const val TAG = "GetWeatherForecast"
    }
    override suspend fun execute(parameters: QueryForecastParameter): RepoResponse<WeatherForecastEntity>  {
        Log.d(TAG,"GetWeatherForecast execute with parameter $parameters")
        return mWeatherRepository.query(parameters.city, parameters.setting.daysOfForecast)
    }

}<file_sep>package com.quypham.vdc.api

import android.util.Log
import com.google.gson.Gson
import com.quypham.vdc.Constants
import retrofit2.Response
import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.net.UnknownHostException


suspend fun <T: Any?> handleRequest(requestFunc: suspend () -> Response<T>): RepoResponse<T> {
    return try {
        requestFunc.invoke().createRepoResponse()
    } catch (exception: Exception) {
        Log.d("XXX","handleRequest exception $exception")
        when (exception) {
            is SocketTimeoutException -> {
                Failure(Constants.INTERNAL_HTTP_CLIENT_TIMEOUT, null, exception)
            }

            is UnknownHostException -> {
                Failure(Constants.INTERNAL_HTTP_NO_INTERNET_CONNECTION, null, exception)
            }

            else -> {
                Failure(Constants.INTERNAL_HTTP_UNKNOWN_ERROR, null, exception)
            }
        }
    }
}

fun <T> Response<T>.createRepoResponse(): RepoResponse<T> {
    let { response ->
        Log.d("XXX","createRepoResponse response $response")
        return try {
            createSuccessResponse(response)
        } catch (t: Throwable) {
            createFailure(t)
        }
    }
}

private fun <T> createSuccessResponse(response: Response<T>): RepoResponse<T> {
    return if (response.isSuccessful) {
        val body = response.body()
        var isFromCache = false
        if (response.raw().cacheResponse != null) {
            isFromCache = true
        }

        if (response.raw().networkResponse != null && response.raw().networkResponse!!.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
            isFromCache = false
        }
        Success(response = body, isFromCache = isFromCache)
    } else {
        try {
            Log.d("XXX","createSuccessResponse error response $response")
            val errorContent = response.errorBody()?.string() ?: response.message()
            val serverError = Gson().fromJson(errorContent, ServerError::class.java)
            Failure(response.code(), serverError, null)
        } catch (exception: Exception) {
            Failure(response.code(), null, null)
        }
    }
}

private fun <T> createFailure(throwable: Throwable): Failure<T> {
    Log.d("XXX","createFailure $throwable")
    return when (throwable) {
        is SocketTimeoutException -> {
            Failure(Constants.INTERNAL_HTTP_CLIENT_TIMEOUT, null, throwable)
        }

        is UnknownHostException -> {
            Failure(Constants.INTERNAL_HTTP_NO_INTERNET_CONNECTION, null, throwable)
        }

        else -> {
            Failure(Constants.INTERNAL_HTTP_UNKNOWN_ERROR, null, throwable)
        }
    }
}

<file_sep>package com.quypham.vdc.ui.weather

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.viewModels
import com.quypham.vdc.base.BaseFragment
import com.quypham.vdc.databinding.FragmentWeatherInfoBinding
import com.quypham.vdc.ui.setting.SettingFragment
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class WeatherInfoFragment : BaseFragment() {
    companion object {
        const val TAG = "MainFragment"

        fun newInstance(): WeatherInfoFragment {
            return WeatherInfoFragment()
        }
    }

    private val mViewModel: WeatherInfoViewModel by viewModels()
    private lateinit var mBinding: FragmentWeatherInfoBinding

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        mBinding = FragmentWeatherInfoBinding.inflate(layoutInflater, container, false)
        return mBinding.root
    }

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

        mBinding.etCity.doOnTextChanged { text, _, _, _ ->
            mBinding.rvWeathers.showLoadingView()
            mViewModel.onQueryChange(text.toString())
        }

        mBinding.setting.setOnClickListener {
            val settingFragment = SettingFragment()
            settingFragment.show(childFragmentManager, settingFragment.tag)
        }

        mViewModel.weatherInfos.observe(viewLifecycleOwner, { response ->
            Log.d(TAG, "onWeatherResponse $response")

            if (response.isSuccess) {
                mBinding.rvWeathers.showRecyclerView()
                mBinding.rvWeathers.recyclerView.apply {
                    if (this.adapter == null) {
                        this.adapter = WeatherInfoAdapter()
                    }
                    (this.adapter as WeatherInfoAdapter).apply {
                        Log.d(TAG, "submit new list ${response.weatherInfoList}")
                        submitList(response.weatherInfoList)
                    }
                }
            } else {
                mBinding.rvWeathers.showErrorView(response.errorMessage)
            }
        })
    }

}<file_sep>package com.quypham.vdc.ui.setting

import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.quypham.vdc.Constants
import com.quypham.vdc.data.TempUnit
import com.quypham.vdc.data.UserSetting
import com.quypham.vdc.di.AppScope
import com.quypham.vdc.domain.UpdateUserSetting
import com.quypham.vdc.repo.SettingRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

class SettingViewModel @ViewModelInject constructor(
    private val settingRepository: SettingRepository,
    private val updateUserSetting: UpdateUserSetting,
    @AppScope private val appScope: CoroutineScope
) : ViewModel() {
    companion object {
        const val TAG = "SettingViewModel"
    }

    private val _userSetting = MutableLiveData<UserSetting?>()

    init {
         viewModelScope.launch {
            _userSetting.value = settingRepository.getSetting()
        }
    }

    val userSetting = _userSetting

    fun updateSetting(daysForecast: Int? = null, unit: TempUnit? = null) {
        val currentSetting =
            _userSetting.value ?: UserSetting(Constants.DEFAULT_DAYS_FORECAST, TempUnit.KELVIN)
        daysForecast?.let {
            currentSetting.daysOfForecast = daysForecast
        }
        unit?.let {
            currentSetting.tempUnit = unit
        }
        _userSetting.value = currentSetting
    }

    override fun onCleared() {
        appScope.launch {
            updateUserSetting.invoke(_userSetting.value ?: UserSetting(Constants.DEFAULT_DAYS_FORECAST, TempUnit.KELVIN))
        }
        super.onCleared()
    }
}<file_sep>package com.quypham.vdc.repo

import androidx.lifecycle.LiveData
import com.quypham.vdc.Constants
import com.quypham.vdc.data.UserSetting
import com.quypham.vdc.repo.setting.SettingDataSource
import javax.inject.Inject
import javax.inject.Named

interface SettingRepository {
    suspend fun getSetting(): UserSetting?
    fun getSettingLive(): LiveData<UserSetting?>
    suspend fun updateSetting(setting: UserSetting)
}
class SettingRepositoryImp @Inject constructor(@Named(Constants.LOCAL) private val localSource: SettingDataSource) : SettingRepository{
    override suspend fun getSetting(): UserSetting? {
        return localSource.getSetting()
    }

    override fun getSettingLive(): LiveData<UserSetting?> {
        return localSource.getSettingLive()
    }

    override suspend fun updateSetting(setting: UserSetting) {
        return localSource.updateSetting(setting)
    }
}<file_sep>package com.quypham.vdc

object Constants {
    //internal http error code
    const val INTERNAL_HTTP_UNKNOWN_ERROR = 600
    const val INTERNAL_HTTP_NO_INTERNET_CONNECTION = 601
    const val INTERNAL_HTTP_CLIENT_TIMEOUT = 408
    const val UNSATISFIABLE_REQUEST= 504
    const val INTERNAL_HTTP_NO_RESULT = 602

    //weather api base url
    const val BASE_URL = "https://api.openweathermap.org/data/2.5/"
    const val QUERY_APP_ID = "60c6fbeb4b93ac653c492ba806fc346d"

    //database constant
    const val APP_DATABASE_VERSION = 1
    const val APP_DATABASE_NAME = "app.db"

    const val MINIMUM_DAYS_FORECAST = 1
    //tested on  weather api,  this could be larger but  user will receive error  anyway
    const val MAXIMUM_DAYS_FORECAST = 17
    const val DEFAULT_DAYS_FORECAST = 5
    const val MINIMUM_CITY_CHARACTERS_ALLOW = 3

    const val LOCAL = "local"
    const val REMOTE = "remote"
}<file_sep># VDC-Weather-Forecast

Build Environment:
- Android Studio 4.1.1
- Build tools: 29
- Target SDK: 29

Summary:
This app is written in Kotlin with MVVM pattern and follow Android Architecture Component including:
- ViewModel
- LiveData & Flow
- View Binding
- Room
- Hilt
- Coroutines

Done:
- Query weather from Weather API and render on UI
- Ability to change setting including temperature unit and days of forecast
- Handling exception and show meaningful explaination on UI
- Caching mechanism based on HTTP Caching

Things could be improved:
- Add more caching mechanism on local database on both memory and file.
- Paging for list weather but WeatherAPI just return maximum of 17 records per city so this is not neccessary.
- Add custom style and themes so that we can changed app UI quickly.
- Accessibility support
<file_sep>package com.quypham.vdc.database

import androidx.lifecycle.LiveData
import androidx.room.*
import com.quypham.vdc.data.UserSetting

@Dao
interface UserSettingDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun updateUserSetting(setting: UserSetting)

    @Query("SELECT * FROM userSetting")
    fun getUserSettingLive(): LiveData<UserSetting?>

    @Query("SELECT * FROM userSetting")
    suspend fun getUserSetting(): UserSetting?
}<file_sep>package com.quypham.vdc.api

import com.quypham.vdc.Constants
import com.quypham.vdc.data.WeatherForecastEntity
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query

interface ApiService {
    @GET("forecast/daily")
    suspend fun queryForecastDaily(@Query("q") city: String,
                           @Query("cnt") daysOfForecast: Int,
                           @Query("appid") appId: String = Constants.QUERY_APP_ID): Response<WeatherForecastEntity?>
}<file_sep>package com.quypham.vdc.api

data class ServerError constructor(var cod: String, var message: String)<file_sep>include ':app'
rootProject.name = "VDC Apps"<file_sep>package com.quypham.vdc.data

data class QueryForecastParameter(val city: String, val setting: UserSetting)<file_sep>package com.quypham.vdc.database

import androidx.room.TypeConverter
import com.quypham.vdc.data.TempUnit


class TempUnitConverter {
    @TypeConverter
    fun fromTempUnitToString(tempUnit: TempUnit): String  {
        return tempUnit.name
    }

    @TypeConverter
    fun fromStringToTempUnit(value: String): TempUnit {
        return TempUnit.valueOf(value)
    }
}<file_sep>package com.quypham.vdc.domain

import android.util.Log
import com.quypham.vdc.UseCase
import com.quypham.vdc.data.UserSetting
import com.quypham.vdc.di.IoDispatcher
import com.quypham.vdc.repo.SettingRepository
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject

class UpdateUserSetting  @Inject constructor(private val settingRepository: SettingRepository,
                                             @IoDispatcher dispatcher: CoroutineDispatcher
) : UseCase<UserSetting, Any>(dispatcher) {
    companion object {
        const val TAG = "UpdateUserSetting"
    }
    override suspend fun execute(parameters: UserSetting): Any {
        Log.d(TAG,"UpdateUserSetting execute")
        settingRepository.updateSetting(parameters)
        return Any()
    }

}<file_sep>package com.quypham.vdc.repo.setting

import androidx.lifecycle.LiveData
import com.quypham.vdc.data.UserSetting
import com.quypham.vdc.database.AppDatabase
import javax.inject.Inject

class SettingLocalDataSource @Inject constructor(private val appDatabase: AppDatabase): SettingDataSource {
    override fun getSettingLive(): LiveData<UserSetting?> {
        return appDatabase.userSettingDao().getUserSettingLive()
    }

    override suspend fun getSetting(): UserSetting? {
        return appDatabase.userSettingDao().getUserSetting()
    }

    override suspend fun updateSetting(setting: UserSetting) {
        val currentSetting = appDatabase.userSettingDao().getUserSetting() ?: setting
        currentSetting.daysOfForecast = setting.daysOfForecast
        currentSetting.tempUnit = setting.tempUnit
        return appDatabase.userSettingDao().updateUserSetting(currentSetting)
    }
}<file_sep>package com.quypham.vdc.ui.setting

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.fragment.app.viewModels
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.quypham.vdc.Constants
import com.quypham.vdc.R
import com.quypham.vdc.data.TempUnit
import com.quypham.vdc.databinding.FragmentSettingBinding
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class SettingFragment : BottomSheetDialogFragment()  {
    companion object {
        fun newInstance(): SettingFragment {
            return SettingFragment()
        }
    }

    private val mViewModel: SettingViewModel by viewModels()
    private lateinit var mBinding: FragmentSettingBinding

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        mBinding = FragmentSettingBinding.inflate(layoutInflater, container, false)
        return mBinding.root
    }

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

        mBinding.tvImperial.setOnClickListener {
            mViewModel.updateSetting(unit = TempUnit.IMPERIAL)
        }

        mBinding.tvMetric.setOnClickListener {
            mViewModel.updateSetting(unit = TempUnit.METRIC)
        }
        
        mBinding.tvKelvin.setOnClickListener {
            mViewModel.updateSetting(unit = TempUnit.KELVIN)
        }

        mBinding.npDaysForecast.apply {
            this.minValue = Constants.MINIMUM_DAYS_FORECAST
            this.maxValue = Constants.MAXIMUM_DAYS_FORECAST
            this.setOnValueChangedListener { _, _, newVal ->
                mViewModel.updateSetting(daysForecast = newVal)
            }
        }

        mViewModel.userSetting.observe(viewLifecycleOwner, {
            mBinding.npDaysForecast.value = it?.daysOfForecast ?: Constants.DEFAULT_DAYS_FORECAST
            switchMetric(it?.tempUnit ?: TempUnit.KELVIN)
        })

    }

    private fun switchMetric(unit: TempUnit) {
        mBinding.apply {
            this.tvKelvin.background = ContextCompat.getDrawable(requireContext(), R.drawable.outline_white_background)
            this.tvImperial.background = ContextCompat.getDrawable(requireContext(), R.drawable.end_rounded_white_background)
            this.tvMetric.background = ContextCompat.getDrawable(requireContext(), R.drawable.start_rounded_white_background)
            this.tvMetric.setTextColor(ContextCompat.getColor(requireContext(),R.color.black))
            this.tvImperial.setTextColor(ContextCompat.getColor(requireContext(),R.color.black))
            this.tvKelvin.setTextColor(ContextCompat.getColor(requireContext(),R.color.black))

            when (unit) {
                TempUnit.METRIC -> {
                    this.tvMetric.background = ContextCompat.getDrawable(requireContext(), R.drawable.start_rounded_black_background)
                    this.tvMetric.setTextColor(ContextCompat.getColor(requireContext(),R.color.white))
                }
                TempUnit.IMPERIAL -> {
                    this.tvImperial.background = ContextCompat.getDrawable(requireContext(), R.drawable.end_rounded_black_background)
                    this.tvImperial.setTextColor(ContextCompat.getColor(requireContext(),R.color.white))
                }
                TempUnit.KELVIN -> {
                    this.tvKelvin.background = ContextCompat.getDrawable(requireContext(), R.drawable.outline_black_background)
                    this.tvKelvin.setTextColor(ContextCompat.getColor(requireContext(),R.color.white))
                }
            }
        }
    }
}<file_sep>package com.quypham.vdc.repo.weather

import com.quypham.vdc.api.RepoResponse
import com.quypham.vdc.data.WeatherForecastEntity
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class WeatherLocalDataSource @Inject constructor(): WeatherDataSource {
    override suspend fun query(city: String, daysOfForecast: Int): RepoResponse<WeatherForecastEntity> {
        throw Exception("not implemented")
    }
}<file_sep>package com.quypham.vdc.ui.weather

import android.content.Context
import android.util.Log
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.*
import com.quypham.vdc.Constants
import com.quypham.vdc.Constants.DEFAULT_DAYS_FORECAST
import com.quypham.vdc.Constants.MINIMUM_CITY_CHARACTERS_ALLOW
import com.quypham.vdc.R
import com.quypham.vdc.api.Failure
import com.quypham.vdc.api.Success
import com.quypham.vdc.data
import com.quypham.vdc.data.QueryForecastParameter
import com.quypham.vdc.data.TempUnit
import com.quypham.vdc.data.UserSetting
import com.quypham.vdc.domain.GetWeatherForecast
import com.quypham.vdc.repo.SettingRepository
import com.quypham.vdc.utils.CommonUtils
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

class WeatherInfoViewModel @ViewModelInject constructor(
    private val getWeatherForecast: GetWeatherForecast,
    settingRepository: SettingRepository,
    @ApplicationContext private val context: Context
) :
    ViewModel() {
    companion object {
        const val TAG = "WeatherInfoViewModel"
    }

    private val _queryParameterLive = MutableLiveData<QueryForecastParameter>()
    private val _settingLive = settingRepository.getSettingLive()
    private var _currentSetting = UserSetting(DEFAULT_DAYS_FORECAST, TempUnit.KELVIN)

    //display symbol
    private val celsiusSymbol by lazy {
        context.getString(R.string.unit_metric)
    }
    private val kelvinSymbol by lazy {
        context.getString(R.string.unit_kelvin)
    }
    private val fahrenheitSymbol by lazy {
        context.getString(R.string.unit_imperial)
    }

    private val _weatherQueryResponse =
        _queryParameterLive.switchMap<QueryForecastParameter, WeatherUiResponse> {
            liveData {
                emit(
                    when {
                        it.city.isEmpty() -> {
                            WeatherUiResponse(
                                isSuccess = false,
                                errorMessage = context.getString(R.string.empty_city_query)
                            )
                        }
                        it.city.length < MINIMUM_CITY_CHARACTERS_ALLOW -> {
                            WeatherUiResponse(
                                isSuccess = false,
                                errorMessage = context.getString(R.string.invalid_city_name)
                            )
                        }
                        else -> {
                            val response = getWeatherForecast(it).data
                            Log.d(TAG, "getDailyForecast result $response")
                            when (response) {
                                is Success -> {
                                    //convert weather model to ui expected model
                                    if (response.response != null) {
                                        if (response.response.list.isNotEmpty()) {
                                            val correctUnitData =
                                                response.response.list.map { weatherItem ->
                                                    val tempSymbol: String
                                                    val weatherDesc =
                                                        if (weatherItem.weather.isNotEmpty()) {
                                                            weatherItem.weather[0].description
                                                        } else {
                                                            context.getString(R.string.description_not_found)
                                                        }

                                                    val averageTemp =
                                                        (weatherItem.temp.max + weatherItem.temp.min) / 2
                                                    val correctAverageTempWithUnit =
                                                        when (it.setting.tempUnit) {
                                                            TempUnit.METRIC -> {
                                                                tempSymbol = celsiusSymbol
                                                                CommonUtils.kelvinToCelsius(
                                                                    averageTemp
                                                                )
                                                                    .toInt()
                                                            }
                                                            TempUnit.IMPERIAL -> {
                                                                tempSymbol = fahrenheitSymbol
                                                                CommonUtils.kelvinToFahrenheit(
                                                                    averageTemp
                                                                ).toInt()
                                                            }
                                                            else -> {
                                                                tempSymbol = kelvinSymbol
                                                                averageTemp.toInt()
                                                            }
                                                        }

                                                    WeatherInfo(
                                                        weatherItem.dt,
                                                        correctAverageTempWithUnit,
                                                        weatherItem.pressure,
                                                        weatherItem.humidity,
                                                        weatherDesc,
                                                        tempSymbol
                                                    )
                                                }

                                            WeatherUiResponse(
                                                true,
                                                weatherInfoList = correctUnitData
                                            )
                                        } else {
                                            WeatherUiResponse(
                                                isSuccess = false,
                                                errorMessage = context.getString(R.string.empty_result_error)
                                            )
                                        }
                                    } else {
                                        WeatherUiResponse(
                                            isSuccess = false,
                                            errorMessage = context.getString(R.string.general_error)
                                        )
                                    }
                                }

                                is Failure -> {
                                    val httpCodeErrorMessage = when (response.code) {
                                        Constants.UNSATISFIABLE_REQUEST -> context.getString(R.string.no_internet_connection)
                                        Constants.INTERNAL_HTTP_CLIENT_TIMEOUT -> context.getString(
                                            R.string.network_timeout
                                        )
                                        else -> context.getString(R.string.general_error)
                                    }
                                    WeatherUiResponse(
                                        isSuccess = false,
                                        errorMessage = response.serverError?.message
                                            ?: httpCodeErrorMessage
                                    )
                                }
                                else -> {
                                    WeatherUiResponse(
                                        isSuccess = false,
                                        errorMessage = context.getString(R.string.general_error)
                                    )
                                }
                            }
                        }
                    }
                )
            }

        }

    init {
        viewModelScope.launch {
            _settingLive.asFlow().collect { updatedSetting ->
                if (updatedSetting == null) return@collect

                //process data when setting is changed
                _queryParameterLive.value?.let {
                    //day of forecast change, re query this city data
                    it.setting.daysOfForecast = updatedSetting.daysOfForecast
                    it.setting.tempUnit = updatedSetting.tempUnit
                    _queryParameterLive.postValue(it)
                }

                _currentSetting = updatedSetting
            }
        }
    }

    val weatherInfos: LiveData<WeatherUiResponse> = _weatherQueryResponse

    fun onQueryChange(city: String) {
        Log.d(TAG, "onQueryChange city $city")
        _queryParameterLive.postValue(QueryForecastParameter(city, _currentSetting))
    }
}

data class WeatherInfo constructor(
    var timestamp: Long, var averageTemp: Int, var pressure: Int,
    var humidity: Int, var description: String, var tempUnit: String
)

data class WeatherUiResponse(
    val isSuccess: Boolean, val errorMessage: String = "",
    val weatherInfoList: List<WeatherInfo>? = null
)
<file_sep>package com.quypham.vdc.api

import android.util.Log
import com.google.gson.Gson
import com.quypham.vdc.Constants
import retrofit2.Response
import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.net.UnknownHostException

sealed class RepoResponse<T> {
    companion object {
        fun <T> create(error: Throwable): Failure<T> {
            Log.d("XXX","handleRequest error $error")
            return when (error) {
                is SocketTimeoutException -> {
                    Failure(Constants.INTERNAL_HTTP_CLIENT_TIMEOUT, null, error)
                }

                is UnknownHostException -> {
                    Failure(Constants.INTERNAL_HTTP_NO_INTERNET_CONNECTION, null, error)
                }

                else -> {
                    Failure(Constants.INTERNAL_HTTP_UNKNOWN_ERROR, null, error)
                }
            }
        }

        fun <T> create(response: Response<T>): RepoResponse<T> {
            return if (response.isSuccessful) {
                val body = response.body()
                var isFromCache = false
                if (response.raw().cacheResponse != null) {
                    isFromCache = true
                }

                if (response.raw().networkResponse != null && response.raw().networkResponse!!.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
                    isFromCache = false
                }
                Success(response = body, isFromCache = isFromCache)
            } else {
                Log.d("XXX","create exception $response")
                try {
                    val errorContent = response.errorBody()?.string() ?: response.message()
                    val serverError = Gson().fromJson(errorContent, ServerError::class.java)
                    Failure(response.code(), serverError, null)
                } catch (exception: Exception) {
                    Failure(response.code(), null, null)
                }
            }
        }
    }
}

data class Success<T>(val response: T?, val isFromCache: Boolean = false) : RepoResponse<T>()

data class Failure<T>(val code: Int, val serverError: ServerError?, val throwable: Throwable? = null, val errorItems: String? = null, val data : T? = null) : RepoResponse<T>()

<file_sep>package com.quypham.vdc.utils

import java.text.SimpleDateFormat
import java.util.*

object DateFormatUtils {
    private var WEATHER_INFO_DATE_FORMAT: ThreadLocal<SimpleDateFormat> =
        object : ThreadLocal<SimpleDateFormat>() {
            override fun initialValue(): SimpleDateFormat {
                return SimpleDateFormat("EEE,dd MMM yyyy", Locale.US)
            }
        }


    fun toWeatherDateFormat(timestamp: Long): String {
        val calendarInstance = Calendar.getInstance()
        calendarInstance.timeInMillis = timestamp*1000
        return WEATHER_INFO_DATE_FORMAT.get()?.format(calendarInstance.time) ?: "Weather format not found"
    }
}<file_sep>package com.quypham.vdc.utils


object CommonUtils {
    fun kelvinToCelsius(kelvinDegree: Double): Double {
        return kelvinDegree - 273.15
    }

    fun kelvinToFahrenheit(kelvinDegree: Double): Double {
        return (kelvinDegree - 273.15) * 1.8 + 32.0
    }
}<file_sep>package com.quypham.vdc.repo.weather

import android.util.Log
import com.quypham.vdc.Constants
import com.quypham.vdc.api.*
import com.quypham.vdc.data.WeatherForecastEntity
import javax.inject.Inject

class WeatherRemoteDataSource @Inject constructor(private val mApiService: ApiService): WeatherDataSource {
    companion object {
        const val TAG = "WeatherRemoteDataSource"
    }

    override suspend fun query(city: String, daysOfForecast: Int): RepoResponse<WeatherForecastEntity> {
        val response = handleRequest {
            mApiService.queryForecastDaily(city, daysOfForecast)
        }
        Log.d(TAG,"query of $city in $daysOfForecast days response $response")
        return when (response) {
            is Success -> {
                val weatherRemotes = response.response

                if (weatherRemotes != null) {
                    Success(weatherRemotes)
                } else {
                    Failure(Constants.INTERNAL_HTTP_NO_RESULT, null, null, null,null)
                }
            }
            is Failure -> {
                Failure(response.code, response.serverError, null, null,null)
            }
        }

    }
}<file_sep>package com.quypham.vdc.ui

import android.os.Bundle
import com.quypham.vdc.R
import com.quypham.vdc.base.BaseActivity
import com.quypham.vdc.ui.weather.WeatherInfoFragment
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity: BaseActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity)
        var infoFragment: WeatherInfoFragment? = supportFragmentManager.findFragmentByTag(WeatherInfoFragment.TAG) as WeatherInfoFragment?

        if (infoFragment == null) { // first time
            infoFragment = WeatherInfoFragment.newInstance()
        }

        addFragment(infoFragment, WeatherInfoFragment.TAG)
    }

    override fun onBackPressed() {}
}<file_sep>package com.quypham.vdc.repo.setting

import androidx.lifecycle.LiveData
import com.quypham.vdc.data.UserSetting

interface SettingDataSource {
    fun getSettingLive(): LiveData<UserSetting?>
    suspend fun getSetting(): UserSetting?
    suspend fun updateSetting(setting: UserSetting)
}<file_sep>package com.quypham.vdc.api

import okhttp3.Cache
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.io.File


object ServiceGenerator {
    private const val CACHE_SIZE: Long = 10 * 1024 * 1024 // 10 MB
    private const val CACHE_FILE_NAME = "cacheResponse"
    private val httpLoggingInterceptor = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger.DEFAULT).apply {
        this.level = HttpLoggingInterceptor.Level.BODY
    }
    private val mRetrofitBuilder = Retrofit.Builder().addConverterFactory(GsonConverterFactory.create())
                                                     .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    private var mRetrofit: Retrofit? = null
    private var mCache: Cache? = null
    private val mHttpClientBuilder = OkHttpClient.Builder()

    fun setBaseUrl(baseUrl: String) {
        mRetrofit = mRetrofitBuilder.baseUrl(baseUrl)
                        .client(mHttpClientBuilder.build())
                        .build()
    }

    fun setInterceptor(interceptor: Interceptor) {
        mHttpClientBuilder.interceptors().clear()
        mHttpClientBuilder.addInterceptor(httpLoggingInterceptor)
        if (!mHttpClientBuilder.interceptors().contains(interceptor)) {
            mHttpClientBuilder.addInterceptor(interceptor)
        }
        mRetrofitBuilder.client(mHttpClientBuilder.build())
    }

    fun setCache(cacheDir: File) {
        if (mCache == null) {
            val file = File(cacheDir.absolutePath, CACHE_FILE_NAME)
            if (!file.exists()) {
                file.mkdir()
            }
            mCache = Cache(file, CACHE_SIZE)
        }
        mHttpClientBuilder.cache(mCache)
    }

    fun <S> createService(serviceClass: Class<S>): S = mRetrofit!!.create(serviceClass)
}<file_sep>plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.2"

    defaultConfig {
        multiDexEnabled = true
        applicationId "com.quypham.vdc"
        minSdkVersion 16
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildFeatures {
        viewBinding true
    }


    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    // android
    implementation "androidx.multidex:multidex:$multidexVersion"
    implementation "androidx.cardview:cardview:$supportLibraryVersion"
    implementation "androidx.appcompat:appcompat:$androidXAppCompatVersion"
    implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
    implementation "androidx.annotation:annotation:$supportLibraryVersion"
    implementation "androidx.constraintlayout:constraintlayout:$constraintVersion"
    implementation group: 'androidx.lifecycle', name: 'lifecycle-viewmodel-ktx', version: "$lifecycle_version"
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'

    //hilt
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
    kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
    annotationProcessor 'androidx.hilt:hilt-compiler:1.0.0-alpha02'

    // google
    implementation "com.google.android.gms:play-services-gcm:$playServiceGcm"
    implementation "com.google.android.gms:play-services-auth:$playServiceAuth"
    implementation "com.google.android.gms:play-services-maps:$playServiceMaps"
    implementation "com.google.android.gms:play-services-fitness:$playServiceFitness"
    implementation "com.google.android.gms:play-services-location:$playServiceLocation"
    implementation "com.google.code.gson:gson:$gsonVersion"

    // google recommend
    implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
    implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
    implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"
    implementation "com.squareup.okhttp3:logging-interceptor:$loggingInterceptor"
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    kapt "com.google.dagger:dagger-compiler:$daggerVersion"
    kapt "com.google.dagger:dagger-android-processor:$daggerVersion"

    // Coroutines
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesCoreVersion"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesAndroidVersion"

    //Parceler
    implementation 'com.squareup:otto:1.3.8'
    implementation "org.parceler:parceler-api:$parcelerVersion"
    annotationProcessor "org.parceler:parceler:$parcelerVersion"


    //dagger
    implementation "com.google.dagger:dagger:$daggerVersion"
    implementation "com.google.dagger:dagger-android:$daggerVersion"
    implementation "com.google.dagger:dagger-android-support:$daggerVersion"
    kapt "com.google.dagger:dagger-compiler:$daggerVersion"
    kapt "com.google.dagger:dagger-android-processor:$daggerVersion"

    //jetpack
    implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
    implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "com.google.android.material:material:1.2.1"

    //ktx
    implementation "androidx.room:room-ktx:2.2.6"
    implementation "androidx.fragment:fragment-ktx:$fragment_version"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
    implementation "androidx.fragment:fragment-ktx:$fragment_version"
}

/**
 * This line should be at the BOTTOM always
 * */
apply plugin: 'com.google.gms.google-services'
<file_sep>// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    ext.kotlin_version = "1.4.20"
    ext.googleServicesVersion = '4.3.4'
    ext.hiltVersion = "2.28-alpha"
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.1.1"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "com.google.gms:google-services:$googleServicesVersion"
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hiltVersion"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}


ext {
    // Sdk and tools
    minSdkVersion = 21
    targetSdkVersion = 29
    compileSdkVersion = 29
    buildToolsVersion = '29.0.3'
    gsonVersion = '2.8.6'
    daggerVersion = '2.30.1'
    multidexVersion = '2.0.1'
    constraintVersion = '2.0.4'
    androidXAppCompatVersion = '1.2.0-rc01'
    playServiceGcm = '17.0.0'
    playServiceAuth = '19.0.0'
    playServiceMaps = '17.0.0'
    playServiceFitness = '20.0.0'
    playServiceLocation = '17.1.0'
    firebaseMessagingVersion = '20.2.3'
    supportLibraryVersion = '1.0.0'
    recyclerViewVersion = '1.1.0'
    materialDesignVersion = '1.1.0'
    coroutinesCoreVersion = '1.3.7'
    coroutinesAndroidVersion = '1.4.1'
    retrofitVersion = '2.9.0'
    lifecycle_version = '2.2.0'
    room_version = '2.2.6'
    parcelerVersion = '1.1.12'
    fragment_version = '1.2.5'
    loggingInterceptor = '4.9.0'
    lifecycleVersion = '2.2.0'
    hiltVersion = "2.28-alpha"


    // google-services.json
    googleServicesJson = 'google-services.json'
}
<file_sep>package com.quypham.vdc.data

class WeatherForecastEntity constructor(var city: WeatherCity,
                                        var cod: String,
                                        var message: Double,
                                        var cnt: Int,
                                        var list: List<WeatherDetail>)

class WeatherCity constructor(var id: Int,
                              var name: String,
                              var coord: Coordinate,
                              var country: String,
                              var population: Int,
                              var timezone: Int)

class Coordinate constructor(var lat: Double,
                             var lng: Double)

class WeatherDetail constructor(var dt: Long,
                                var sunrise: Long,
                                var sunset: Long,
                                var temp: WeatherTemp,
                                var feels_like: WeatherFeelLike,
                                var pressure: Int,
                                var humidity: Int,
                                var weather: List<WeatherDescription>,
                                var speed: Double,
                                var deg: Int,
                                var clouds: Int,
                                var pop: Double)

class WeatherTemp constructor(var day: Double,
                              var min: Double,
                              var max: Double,
                              var night: Double,
                              var eve: Double,
                              var mom: Double)

class WeatherFeelLike constructor(var day: Double,
                                  var night: Double,
                                  var eve: Double,
                                  var mom: Double)

class WeatherDescription constructor(var id: Int, var main: String, var description: String, var icon: String)<file_sep>package com.quypham.vdc.data

import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.quypham.vdc.database.TempUnitConverter

@Entity(tableName = "userSetting")
@TypeConverters(TempUnitConverter::class)
data class UserSetting constructor(var daysOfForecast: Int, var tempUnit: TempUnit) {
    @PrimaryKey(autoGenerate = true)
    var id: Int = 0
}

enum class TempUnit {
    KELVIN, METRIC, IMPERIAL
}<file_sep>package com.quypham.vdc.ui.weather

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.quypham.vdc.R
import com.quypham.vdc.databinding.ItemWeatherBinding
import com.quypham.vdc.utils.DateFormatUtils

class WeatherInfoAdapter :
    ListAdapter<WeatherInfo, WeatherInfoAdapter.WeatherInfoViewHolder>(WeatherDiff) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeatherInfoViewHolder {
        return WeatherInfoViewHolder(
            ItemWeatherBinding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false
            ), parent.context
        )
    }

    override fun onBindViewHolder(holder: WeatherInfoViewHolder, position: Int) {
        holder.bindData(getItem(position))
    }

    class WeatherInfoViewHolder(
        private val binding: ItemWeatherBinding,
        private val context: Context
    ) : RecyclerView.ViewHolder(binding.root) {
        fun bindData(weatherInfo: WeatherInfo) {
            binding.tvDate.text = String.format(
                context.getString(R.string.weather_date_format),
                DateFormatUtils.toWeatherDateFormat(weatherInfo.timestamp)
            )
            binding.tvAvgTemp.text = String.format(
                context.getString(R.string.weather_avg_temp_format),
                weatherInfo.averageTemp,
                weatherInfo.tempUnit
            )
            binding.tvPressure.text = String.format(
                context.getString(R.string.weather_pressure_format),
                weatherInfo.pressure
            )
            binding.tvHumidity.text = String.format(
                context.getString(R.string.weather_humidity_format),
                weatherInfo.humidity
            )
            binding.tvDescription.text = String.format(
                context.getString(R.string.weather_description_format),
                weatherInfo.description
            )
        }
    }
}

object WeatherDiff : DiffUtil.ItemCallback<WeatherInfo>() {
    override fun areItemsTheSame(oldItem: WeatherInfo, newItem: WeatherInfo): Boolean {
        return oldItem.timestamp == newItem.timestamp
    }

    override fun areContentsTheSame(oldItem: WeatherInfo, newItem: WeatherInfo): Boolean {
        return oldItem == newItem
    }
}<file_sep>package com.quypham.vdc.di

import com.quypham.vdc.Constants
import com.quypham.vdc.api.ApiService
import com.quypham.vdc.database.AppDatabase
import com.quypham.vdc.repo.SettingRepository
import com.quypham.vdc.repo.SettingRepositoryImp
import com.quypham.vdc.repo.WeatherRepository
import com.quypham.vdc.repo.WeatherRepositoryImp
import com.quypham.vdc.repo.setting.SettingDataSource
import com.quypham.vdc.repo.setting.SettingLocalDataSource
import com.quypham.vdc.repo.weather.WeatherDataSource
import com.quypham.vdc.repo.weather.WeatherLocalDataSource
import com.quypham.vdc.repo.weather.WeatherRemoteDataSource
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ApplicationComponent
import javax.inject.Named
import javax.inject.Singleton

@InstallIn(ApplicationComponent::class)
@Module
class RepositoryModule {
    @Singleton
    @Provides
    @Named(Constants.LOCAL)
    fun provideLocalUserDataSource(): WeatherDataSource {
        return WeatherLocalDataSource()
    }

    @Singleton
    @Provides
    @Named(Constants.REMOTE)
    fun provideRemoteUserDataSource(apiService: ApiService): WeatherDataSource {
        return WeatherRemoteDataSource(apiService)
    }

    @Singleton
    @Provides
    fun provideUserRepository(@Named(Constants.REMOTE) remoteSource: WeatherDataSource): WeatherRepository {
        return WeatherRepositoryImp(remoteSource)
    }

    @Singleton
    @Provides
    @Named(Constants.LOCAL)
    fun provideLocalSettingDataSource(appDatabase: AppDatabase): SettingDataSource {
        return SettingLocalDataSource(appDatabase)
    }

    @Singleton
    @Provides
    fun provideSettingRepository(@Named(Constants.LOCAL) localSource: SettingDataSource): SettingRepository {
        return SettingRepositoryImp(localSource)
    }
}<file_sep>package com.quypham.vdc.repo

import android.util.Log
import com.quypham.vdc.Constants
import com.quypham.vdc.api.RepoResponse
import com.quypham.vdc.data.WeatherForecastEntity
import com.quypham.vdc.repo.weather.WeatherDataSource
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton

interface WeatherRepository {
    suspend fun query(city: String, daysOfForecast: Int): RepoResponse<WeatherForecastEntity>
}

@Singleton
class WeatherRepositoryImp @Inject constructor(@Named(Constants.REMOTE) private val mWeatherRemoteDataSource: WeatherDataSource): WeatherRepository {
    companion object {
        const val TAG = "WeatherRepository"
    }
    override suspend fun query(city: String, daysOfForecast: Int): RepoResponse<WeatherForecastEntity> {
        Log.d(TAG,"query with city $city days $daysOfForecast")
        return mWeatherRemoteDataSource.query(city, daysOfForecast)
    }
}<file_sep>package com.quypham.vdc.customview

import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import androidx.annotation.DrawableRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import com.quypham.vdc.R
import com.quypham.vdc.databinding.RecyclerErrorViewBinding
import com.quypham.vdc.databinding.RecyclerLoadingViewBinding
import com.quypham.vdc.databinding.StateRecyclerViewBinding

class StateRecyclerView constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {

    constructor(context: Context) : this(context, null, 0)

    constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0)

    private val mBinding: StateRecyclerViewBinding = StateRecyclerViewBinding.inflate(
        LayoutInflater.from(context), this
    )

    private val mErrorViewBinding: RecyclerErrorViewBinding

    private val mLoadingViewBinding: RecyclerLoadingViewBinding

    val recyclerView: RecyclerView
        get() = mBinding.recyclerView

    var errorText: String = ""
        set(value) {
            field = value
            mErrorViewBinding.tvErrorMessage.text = value
        }

    @DrawableRes
    var errorIcon = 0
        set(value) {
            field = value
            mErrorViewBinding.ivError.setImageResource(value)
        }

    init {

        // inflate the layout
        mErrorViewBinding = mBinding.errorView
        mLoadingViewBinding = mBinding.loadingView

        context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.StateRecyclerView,
            0,
            0
        ).apply {
            try {
                errorText = getString(R.styleable.StateRecyclerView_errorText) ?: context.getString(
                    R.string.general_error
                )
                errorIcon = getResourceId(
                    R.styleable.StateRecyclerView_errorIcon,
                    R.drawable.baseline_error_outline_white_48
                )
            } finally {
                recycle()
            }
        }
    }

    fun showRecyclerView() {
        mLoadingViewBinding.root.visibility = View.GONE
        mErrorViewBinding.root.visibility = View.GONE
        recyclerView.visibility = View.VISIBLE
    }

    fun showErrorView(errorMess: String) {
        errorText = errorMess
        mLoadingViewBinding.root.visibility = View.GONE
        recyclerView.visibility = View.GONE
        mErrorViewBinding.root.visibility = View.VISIBLE
    }

    fun showLoadingView() {
        mLoadingViewBinding.root.visibility = View.VISIBLE
        mErrorViewBinding.root.visibility = View.GONE
    }
}<file_sep>package com.quypham.vdc.database

import androidx.room.Database
import androidx.room.RoomDatabase
import com.quypham.vdc.Constants
import com.quypham.vdc.data.UserSetting

@Database(entities = [UserSetting::class], version = Constants.APP_DATABASE_VERSION, exportSchema = false)
abstract class AppDatabase: RoomDatabase() {
    abstract fun userSettingDao(): UserSettingDao
}