diff --git a/app/build.gradle b/app/build.gradle index 3b7d186..b8d444f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,6 @@ buildscript { apply from: "ktlint.gradle" + } plugins { @@ -43,11 +44,15 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' + } + buildFeatures { + viewBinding = true + buildConfig=true } buildFeatures { viewBinding = true @@ -70,15 +75,18 @@ dependencies { implementation 'com.github.bumptech.glide:glide:4.16.0' implementation 'com.airbnb.android:lottie:3.4.0' // Room + implementation "androidx.room:room-ktx:2.6.1" implementation "androidx.room:room-runtime:2.6.1" implementation "androidx.legacy:legacy-support-v4:1.0.0" implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' - annotationProcessor "androidx.room:room-compiler:2.6.1" + kapt "androidx.room:room-compiler:2.6.1" + // retrofit implementation "com.squareup.retrofit2:retrofit:2.11.0" implementation "com.squareup.retrofit2:converter-gson:2.11.0" implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11' // ViewModel implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' annotationProcessor "androidx.lifecycle:lifecycle-compiler:2.7.0" @@ -86,6 +94,11 @@ dependencies { implementation "io.insert-koin:koin-android:3.5.0" // coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3" + // Rx + implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation 'io.reactivex.rxjava2:rxjava:2.2.8' + implementation 'androidx.room:room-rxjava2:2.0.0' // WorkManager implementation 'androidx.work:work-runtime-ktx:2.9.0' + } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 076c1ca..128bf33 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ = (LayoutInflater) -> T @@ -14,6 +15,7 @@ abstract class BaseActivity(private val inflate: ActivityInfla val binding get() = _binding!! abstract val viewModel: ViewModel + abstract val sharedViewModel: SharedViewModel protected abstract fun initialize() diff --git a/app/src/main/java/com/sun/weather/base/BaseFragment.kt b/app/src/main/java/com/sun/weather/base/BaseFragment.kt index 7b8695a..051d305 100644 --- a/app/src/main/java/com/sun/weather/base/BaseFragment.kt +++ b/app/src/main/java/com/sun/weather/base/BaseFragment.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel import androidx.viewbinding.ViewBinding +import com.sun.weather.ui.SharedViewModel typealias FragmentInflate = (LayoutInflater, ViewGroup?, Boolean) -> T @@ -15,6 +16,7 @@ abstract class BaseFragment(private val inflate: FragmentInfla private var _binding: VB? = null val binding get() = _binding!! abstract val viewModel: ViewModel + abstract val sharedViewModel: SharedViewModel protected abstract fun initView() diff --git a/app/src/main/java/com/sun/weather/data/model/CurrentWeather.kt b/app/src/main/java/com/sun/weather/data/model/CurrentWeather.kt index 6e2950b..3f28a61 100644 --- a/app/src/main/java/com/sun/weather/data/model/CurrentWeather.kt +++ b/app/src/main/java/com/sun/weather/data/model/CurrentWeather.kt @@ -1,77 +1,101 @@ package com.sun.weather.data.model +import com.example.weather.utils.ext.combineWithCountry +import com.google.gson.annotations.Expose import com.google.gson.annotations.SerializedName import com.sun.weather.data.model.entity.WeatherEntity import com.sun.weather.utils.ext.combineWithCountry data class CurrentWeather( @SerializedName("main") - var main: Main, + @Expose + var main: Main? = null, @SerializedName("weather") - var weathers: List, + @Expose + var weathers: List? = null, @SerializedName("wind") - var wind: Wind, + @Expose + var wind: Wind? = null, @SerializedName("clouds") - var clouds: Clouds, + @Expose + var clouds: Clouds? = null, @SerializedName("coord") - var coord: Coord, + @Expose + var coord: Coord? = Coord(0.0, 0.0), @SerializedName("dt") - var dt: Long, + @Expose + var dt: Long? = null, @SerializedName("name") - var nameCity: String, + @Expose + var nameCity: String? = null, @SerializedName("sys") - var sys: Sys, - var day: String = "", - var iconWeather: String = "", + @Expose + var sys: Sys? = null, + var day: String? = "", + var iconWeather: String? = "", ) data class Main( @SerializedName("temp") - var currentTemperature: Double, + @Expose + var currentTemperature: Double? = null, @SerializedName("humidity") - var humidity: Int, - @SerializedName("temp_min") var tempMin: Double, - @SerializedName("temp_max") var tempMax: Double, + @Expose + var humidity: Int? = null, + @SerializedName("temp_min") + @Expose + var tempMin: Double? = null, + @SerializedName("temp_max") + @Expose + var tempMax: Double? = null, ) data class Weather( @SerializedName("icon") - var iconWeather: String, + @Expose + var iconWeather: String? = null, @SerializedName("main") - var main: String, + @Expose + var main: String? = null, @SerializedName("description") - var description: String, + @Expose + var description: String? = null, ) data class Wind( @SerializedName("speed") - var windSpeed: Double, + @Expose + var windSpeed: Double? = null, ) data class Clouds( @SerializedName("all") - var percentCloud: Int, + @Expose + var percentCloud: Int? = null, ) data class Coord( @SerializedName("lon") - var lon: Double, + @Expose + var lon: Double? = null, @SerializedName("lat") - var lat: Double, + @Expose + var lat: Double? = null, ) data class Sys( @SerializedName("country") - var country: String, + @Expose + var country: String? = null, ) fun CurrentWeather.toWeatherEntity(): WeatherEntity { - val country: String? = sys.country - val id = nameCity.combineWithCountry(country) - val latitude = coord.lat - val longitude = coord.lon - val timeZone = dt - val city = nameCity + val country: String = sys?.country ?: "Unknown" + val id = nameCity?.combineWithCountry(country) ?: "Unknown" + val latitude = coord?.lat ?: 0.0 + val longitude = coord?.lon ?: 0.0 + val timeZone = dt ?: 0 + val city = nameCity ?: "Unknown" val weatherCurrent: WeatherBasic? = this.toWeatherBasic() val weatherHourlyList: List? = null val weatherDailyList: List? = null @@ -89,16 +113,20 @@ fun CurrentWeather.toWeatherEntity(): WeatherEntity { ) } -fun CurrentWeather.toWeatherBasic(): WeatherBasic { - return WeatherBasic( - dateTime = dt, - currentTemperature = main.currentTemperature, - maxTemperature = main.tempMax, - minTemperature = main.tempMin, - iconWeather = weathers.firstOrNull()?.iconWeather, - weatherDescription = weathers.firstOrNull()?.description, - humidity = main.humidity, - percentCloud = clouds.percentCloud, - windSpeed = wind.windSpeed, - ) +fun CurrentWeather.toWeatherBasic(): WeatherBasic? { + return if (main != null && weathers?.isNotEmpty() == true && wind != null && clouds != null) { + WeatherBasic( + dateTime = dt ?: 0, + currentTemperature = main?.currentTemperature ?: 0.0, + maxTemperature = main?.tempMax ?: 0.0, + minTemperature = main?.tempMin ?: 0.0, + iconWeather = weathers?.firstOrNull()?.iconWeather, + weatherDescription = weathers?.firstOrNull()?.description, + humidity = main?.humidity ?: 0, + percentCloud = clouds?.percentCloud ?: 0, + windSpeed = wind?.windSpeed ?: 0.0, + ) + } else { + null + } } diff --git a/app/src/main/java/com/sun/weather/data/model/HourlyForecast.kt b/app/src/main/java/com/sun/weather/data/model/HourlyForecast.kt index 8bb92de..d0e77de 100644 --- a/app/src/main/java/com/sun/weather/data/model/HourlyForecast.kt +++ b/app/src/main/java/com/sun/weather/data/model/HourlyForecast.kt @@ -1,41 +1,73 @@ package com.sun.weather.data.model +import com.example.weather.utils.ext.combineWithCountry +import com.google.gson.annotations.Expose import com.google.gson.annotations.SerializedName import com.sun.weather.data.model.entity.WeatherEntity import com.sun.weather.utils.ext.combineWithCountry data class HourlyForecast( - @SerializedName("cnt") val cnt: Int, - @SerializedName("list") val forecastList: List, - @SerializedName("city") val city: City, + @SerializedName("cnt") + @Expose + val cnt: Int = 0, + @SerializedName("list") + @Expose + val forecastList: List? = null, + @SerializedName("city") + @Expose + val city: City? = null, ) data class City( - @SerializedName("id") val id: Int, - @SerializedName("name") val name: String, - @SerializedName("coord") val coord: Coord, - @SerializedName("country") val country: String, + @SerializedName("id") + @Expose + val id: Int = 0, + @SerializedName("name") + @Expose + val name: String? = null, + @SerializedName("coord") + @Expose + val coord: Coord? = null, + @SerializedName("country") + @Expose + val country: String? = null, ) data class HourlyForecastItem( - @SerializedName("dt") val dt: Long, - @SerializedName("main") val main: Main, - @SerializedName("weather") val weather: List, - @SerializedName("clouds") val clouds: Clouds, - @SerializedName("wind") val wind: Wind, - @SerializedName("dt_txt") val dtTxt: String, - val iconWeather: String = "", + @SerializedName("dt") + @Expose + val dt: Long = 0, + @SerializedName("main") + @Expose + val main: Main? = null, + @SerializedName("weather") + @Expose + val weather: List? = null, + @SerializedName("clouds") + @Expose + val clouds: Clouds? = null, + @SerializedName("wind") + @Expose + val wind: Wind? = null, + @SerializedName("dt_txt") + @Expose + val dtTxt: String? = null, + val iconWeather: String? = "", ) -fun HourlyForecast.toWeather(): WeatherEntity { +fun HourlyForecast.toWeatherEntity(): WeatherEntity { + val cityName = city?.name ?: "Unknown" + val cityCountry = city?.country ?: "Unknown" + val coord = city?.coord ?: Coord(0.0, 0.0) + return WeatherEntity( - id = city.name.combineWithCountry(city.country), - latitude = city.coord.lat, - longitude = city.coord.lon, - city = city.name, - country = city.country, + id = cityName.combineWithCountry(cityCountry), + latitude = coord.lat, + longitude = coord.lon, + city = cityName, + country = cityCountry, weatherCurrent = null, - weatherHourlyList = forecastList.map { it.toWeatherBasic() }, + weatherHourlyList = forecastList?.map { it.toWeatherBasic() } ?: emptyList(), weatherDailyList = null, ) } @@ -43,13 +75,13 @@ fun HourlyForecast.toWeather(): WeatherEntity { fun HourlyForecastItem.toWeatherBasic(): WeatherBasic { return WeatherBasic( dateTime = dt, - currentTemperature = main.currentTemperature, - maxTemperature = main.tempMax, - minTemperature = main.tempMin, - iconWeather = weather.firstOrNull()?.iconWeather, - weatherDescription = weather.firstOrNull()?.description, - humidity = main.humidity, - percentCloud = clouds.percentCloud, - windSpeed = wind.windSpeed, + currentTemperature = main?.currentTemperature ?: 0.0, + maxTemperature = main?.tempMax ?: 0.0, + minTemperature = main?.tempMin ?: 0.0, + iconWeather = weather?.firstOrNull()?.iconWeather, + weatherDescription = weather?.firstOrNull()?.description, + humidity = main?.humidity ?: 0, + percentCloud = clouds?.percentCloud ?: 0, + windSpeed = wind?.windSpeed ?: 0.0, ) } diff --git a/app/src/main/java/com/sun/weather/data/model/WeeklyForecast.kt b/app/src/main/java/com/sun/weather/data/model/WeeklyForecast.kt index 7ec8fe6..92c14c2 100644 --- a/app/src/main/java/com/sun/weather/data/model/WeeklyForecast.kt +++ b/app/src/main/java/com/sun/weather/data/model/WeeklyForecast.kt @@ -1,42 +1,77 @@ package com.sun.weather.data.model + +import com.google.gson.annotations.Expose import com.google.gson.annotations.SerializedName import com.sun.weather.data.model.entity.WeatherEntity import com.sun.weather.utils.ext.combineWithCountry data class WeeklyForecast( - @SerializedName("city") val city: City, - @SerializedName("cnt") val cnt: Int, - @SerializedName("list") val forecastList: List, + @SerializedName("city") + @Expose + val city: City? = null, + @SerializedName("cnt") + @Expose + val cnt: Int = 0, + @SerializedName("list") + @Expose + val forecastList: List? = null, ) data class WeeklyForecastItem( - @SerializedName("dt") val dt: Long, - @SerializedName("temp") val temp: Temp, - @SerializedName("humidity") val humidity: Int, - @SerializedName("weather") val weather: List, - @SerializedName("speed") val speed: Double, - @SerializedName("clouds") val clouds: Int, - val day: String = "", - val iconWeather: String = "", + @SerializedName("dt") + @Expose + val dt: Long = 0, + @SerializedName("temp") + @Expose + val temp: Temp? = null, + @SerializedName("humidity") + @Expose + val humidity: Int = 0, + @SerializedName("weather") + @Expose + val weather: List? = null, + @SerializedName("speed") + @Expose + val speed: Double = 0.0, + @SerializedName("clouds") + @Expose + val clouds: Int = 0, + val day: String? = "", + val iconWeather: String? = "", ) data class Temp( - @SerializedName("day") val day: Double, - @SerializedName("min") val min: Double, - @SerializedName("max") val max: Double, - @SerializedName("night") val night: Double, - @SerializedName("eve") val eve: Double, - @SerializedName("morn") val morn: Double, + @SerializedName("day") + @Expose + val day: Double = 0.0, + @SerializedName("min") + @Expose + val min: Double = 0.0, + @SerializedName("max") + @Expose + val max: Double = 0.0, + @SerializedName("night") + @Expose + val night: Double = 0.0, + @SerializedName("eve") + @Expose + val eve: Double = 0.0, + @SerializedName("morn") + @Expose + val morn: Double = 0.0, ) fun WeeklyForecast.toWeatherEntity(): WeatherEntity { + val cityName = city?.name ?: "Unknown" + val cityCountry = city?.country ?: "Unknown" + val coord = city?.coord ?: Coord(0.0, 0.0) return WeatherEntity( - id = city.name.combineWithCountry(city.country), - latitude = city.coord.lat, - longitude = city.coord.lon, - city = city.name, - country = city.country, - weatherDailyList = forecastList.map { it.toWeatherBasic() }, + id = cityName.combineWithCountry(cityCountry), + latitude = coord.lat, + longitude = coord.lon, + city = cityName, + country = cityCountry, + weatherDailyList = forecastList?.map { it.toWeatherBasic() } ?: emptyList(), weatherHourlyList = null, weatherCurrent = null, ) @@ -45,10 +80,11 @@ fun WeeklyForecast.toWeatherEntity(): WeatherEntity { fun WeeklyForecastItem.toWeatherBasic(): WeatherBasic { return WeatherBasic( dateTime = dt, - currentTemperature = temp.day, - maxTemperature = temp.max, - minTemperature = temp.min, - iconWeather = weather.firstOrNull()?.iconWeather, + currentTemperature = temp?.day ?: 0.0, + maxTemperature = temp?.max ?: 0.0, + minTemperature = temp?.min ?: 0.0, + iconWeather = weather?.firstOrNull()?.iconWeather, + weatherDescription = weather?.firstOrNull()?.description, humidity = humidity, percentCloud = clouds, windSpeed = speed, diff --git a/app/src/main/java/com/sun/weather/data/repository/source/WeatherDataSource.kt b/app/src/main/java/com/sun/weather/data/repository/source/WeatherDataSource.kt index 57c46e2..4e4484a 100644 --- a/app/src/main/java/com/sun/weather/data/repository/source/WeatherDataSource.kt +++ b/app/src/main/java/com/sun/weather/data/repository/source/WeatherDataSource.kt @@ -2,22 +2,22 @@ package com.sun.weather.data.repository.source import com.sun.weather.data.model.CurrentWeather import com.sun.weather.data.model.HourlyForecast -import com.sun.weather.data.model.Weather import com.sun.weather.data.model.WeeklyForecast +import com.sun.weather.data.model.entity.WeatherEntity interface WeatherDataSource { interface Local { suspend fun insertCurrentWeather( - current: Weather, - hourly: Weather, - daily: Weather, + current: WeatherEntity, + hourly: WeatherEntity, + daily: WeatherEntity, ) - suspend fun insertCurrentWeather(weather: Weather) + suspend fun insertCurrentWeather(weather: WeatherEntity) - suspend fun getAllLocalWeathers(): List + suspend fun getAllLocalWeathers(): List - suspend fun getLocalWeather(id: String): Weather? + suspend fun getLocalWeather(id: String): WeatherEntity? suspend fun deleteWeather(id: String) } diff --git a/app/src/main/java/com/sun/weather/data/repository/source/local/AppDatabase.kt b/app/src/main/java/com/sun/weather/data/repository/source/local/AppDatabase.kt index 1f63b97..f5953b2 100644 --- a/app/src/main/java/com/sun/weather/data/repository/source/local/AppDatabase.kt +++ b/app/src/main/java/com/sun/weather/data/repository/source/local/AppDatabase.kt @@ -5,7 +5,6 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters -import com.sun.weather.data.model.Weather import com.sun.weather.data.repository.source.local.converter.WeatherBasicConverter import com.sun.weather.data.repository.source.local.converter.WeatherBasicListConverter import com.sun.weather.data.repository.source.local.dao.WeatherDao diff --git a/app/src/main/java/com/sun/weather/data/repository/source/local/dao/WeatherDao.kt b/app/src/main/java/com/sun/weather/data/repository/source/local/dao/WeatherDao.kt index aee036c..3f09c38 100644 --- a/app/src/main/java/com/sun/weather/data/repository/source/local/dao/WeatherDao.kt +++ b/app/src/main/java/com/sun/weather/data/repository/source/local/dao/WeatherDao.kt @@ -5,21 +5,21 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction -import com.sun.weather.data.model.Weather +import com.sun.weather.data.model.entity.WeatherEntity import com.sun.weather.data.model.entity.WeatherEntry.TBL_WEATHER_NAME @Dao interface WeatherDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertWeather(weather: Weather) + suspend fun insertWeather(weather: WeatherEntity) @Transaction @Query("SELECT * FROM $TBL_WEATHER_NAME") - suspend fun getAllData(): List + suspend fun getAllData(): List @Transaction @Query("SELECT * FROM $TBL_WEATHER_NAME WHERE id = :idWeather") - suspend fun getWeather(idWeather: String): Weather? + suspend fun getWeather(idWeather: String): WeatherEntity? @Query("DELETE FROM $TBL_WEATHER_NAME WHERE id = :idWeather") suspend fun deleteWeather(idWeather: String) diff --git a/app/src/main/java/com/sun/weather/di/AppModule.kt b/app/src/main/java/com/sun/weather/di/AppModule.kt index ca867e3..3a2bc2c 100644 --- a/app/src/main/java/com/sun/weather/di/AppModule.kt +++ b/app/src/main/java/com/sun/weather/di/AppModule.kt @@ -7,18 +7,19 @@ import com.example.weather.utils.dispatchers.DispatcherProvider import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder +import com.sun.weather.data.repository.source.local.AppDatabase import com.sun.weather.data.repository.source.remote.api.middleware.BooleanAdapter import com.sun.weather.data.repository.source.remote.api.middleware.DoubleAdapter import com.sun.weather.data.repository.source.remote.api.middleware.IntegerAdapter -import com.sun.weather.utils.DateTimeUtils +import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val AppModule = module { single { provideResources(get()) } - single { provideBaseDispatcherProvider() } - + single { provideAppDatabase(androidContext()) } + factory { get().weatherDao() } single { provideGson() } } @@ -26,6 +27,10 @@ fun provideResources(app: Application): Resources { return app.resources } +fun provideAppDatabase(context: Context): AppDatabase { + return AppDatabase.getInstance(context) +} + fun provideBaseDispatcherProvider(): BaseDispatcherProvider { return DispatcherProvider() } diff --git a/app/src/main/java/com/sun/weather/di/DataSourceModule.kt b/app/src/main/java/com/sun/weather/di/DataSourceModule.kt index 4e18013..e5fc32b 100644 --- a/app/src/main/java/com/sun/weather/di/DataSourceModule.kt +++ b/app/src/main/java/com/sun/weather/di/DataSourceModule.kt @@ -1,10 +1,12 @@ -package com.example.weather.di +package com.sun.weather.di import com.sun.weather.data.repository.source.WeatherDataSource +import com.sun.weather.data.repository.source.local.WeatherLocalDataSource import com.sun.weather.data.repository.source.remote.WeatherRemoteDataSource import org.koin.dsl.module val DataSourceModule = module { single { WeatherRemoteDataSource(get()) } + single { WeatherLocalDataSource(get()) } } diff --git a/app/src/main/java/com/sun/weather/di/NetworkModule.kt b/app/src/main/java/com/sun/weather/di/NetworkModule.kt index be7a350..1999630 100644 --- a/app/src/main/java/com/sun/weather/di/NetworkModule.kt +++ b/app/src/main/java/com/sun/weather/di/NetworkModule.kt @@ -1,5 +1,5 @@ -package com.example.weather.di +package com.sun.weather.di import android.app.Application import com.google.gson.Gson import com.sun.weather.data.repository.source.remote.api.ApiService @@ -63,6 +63,5 @@ object NetWorkInstant { internal const val READ_TIMEOUT = 60L internal const val WRITE_TIMEOUT = 30L internal const val CONNECT_TIMEOUT = 60L - internal const val CACHE_SIZE = 10 * 1024 * 1024L // 10MB } diff --git a/app/src/main/java/com/sun/weather/di/ViewModelModule.kt b/app/src/main/java/com/sun/weather/di/ViewModelModule.kt new file mode 100644 index 0000000..bafa14a --- /dev/null +++ b/app/src/main/java/com/sun/weather/di/ViewModelModule.kt @@ -0,0 +1,15 @@ +package com.sun.weather.di + +import com.sun.weather.ui.MainViewModel +import com.sun.weather.ui.SharedViewModel +import com.sun.weather.ui.home.HomeViewModel +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.module.Module +import org.koin.dsl.module + +val ViewModelModule: Module = + module { + viewModel { MainViewModel(get()) } + viewModel { SharedViewModel() } + viewModel { HomeViewModel(get()) } + } diff --git a/app/src/main/java/com/sun/weather/ui/MainActivity.kt b/app/src/main/java/com/sun/weather/ui/MainActivity.kt index 0fda193..9079ddb 100644 --- a/app/src/main/java/com/sun/weather/ui/MainActivity.kt +++ b/app/src/main/java/com/sun/weather/ui/MainActivity.kt @@ -1,12 +1,37 @@ package com.sun.weather.ui -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer import com.sun.weather.R +import com.sun.weather.base.BaseActivity +import com.sun.weather.databinding.ActivityMainBinding +import com.sun.weather.ui.home.HomeFragment +import org.koin.androidx.viewmodel.ext.android.viewModel -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) +class MainActivity : BaseActivity(ActivityMainBinding::inflate) { + override val viewModel: MainViewModel by viewModel() + override val sharedViewModel: SharedViewModel by viewModel() + + override fun initialize() { + viewModel.locationData.observe( + this, + Observer { location -> + val (latitude, longitude) = location + // Chuyển dữ liệu tọa độ sang HomeFragment + val homeFragment = HomeFragment.newInstance(latitude, longitude) + setNextFragment(homeFragment) + }, + ) + + // Gọi hàm yêu cầu vị trí người dùng + viewModel.requestLocationAndFetchWeather(this) + } + + private fun setNextFragment(fragment: Fragment) { + supportFragmentManager + .beginTransaction() + .addToBackStack(fragment::javaClass.name) + .replace(R.id.fragment_container, fragment) + .commit() } } diff --git a/app/src/main/java/com/sun/weather/ui/MainViewModel.kt b/app/src/main/java/com/sun/weather/ui/MainViewModel.kt new file mode 100644 index 0000000..75cc51f --- /dev/null +++ b/app/src/main/java/com/sun/weather/ui/MainViewModel.kt @@ -0,0 +1,56 @@ +package com.sun.weather.ui + +import android.Manifest +import android.app.Activity +import android.app.Application +import android.content.pm.PackageManager +import android.location.Location +import android.widget.Toast +import androidx.core.app.ActivityCompat +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.google.android.gms.location.LocationServices +import com.google.android.gms.tasks.Task + +class MainViewModel(application: Application) : AndroidViewModel(application) { + private val _locationData = MutableLiveData>() + val locationData: LiveData> get() = _locationData + + fun requestLocationAndFetchWeather(activity: Activity) { + val context = getApplication().applicationContext + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION, + ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION, + ) != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions( + activity, + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + ), + REQUEST_CODE, + ) + } else { + val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) + val locationTask: Task = fusedLocationProviderClient.lastLocation + locationTask.addOnSuccessListener { location -> + if (location != null) { + val latitude = location.latitude + val longitude = location.longitude + _locationData.value = Pair(latitude, longitude) + } else { + Toast.makeText(context, "Location is null", Toast.LENGTH_SHORT).show() + } + } + } + } + + companion object { + private const val REQUEST_CODE = 1000 + } +} diff --git a/app/src/main/java/com/sun/weather/ui/SharedViewModel.kt b/app/src/main/java/com/sun/weather/ui/SharedViewModel.kt new file mode 100644 index 0000000..8a08ddf --- /dev/null +++ b/app/src/main/java/com/sun/weather/ui/SharedViewModel.kt @@ -0,0 +1,12 @@ +package com.sun.weather.ui + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class SharedViewModel : ViewModel() { + var isNetworkAvailable = MutableLiveData() + + fun checkNetwork(isEnable: Boolean) { + isNetworkAvailable.postValue(isEnable) + } +} diff --git a/app/src/main/java/com/sun/weather/ui/home/HomeFragment.kt b/app/src/main/java/com/sun/weather/ui/home/HomeFragment.kt new file mode 100644 index 0000000..ff3fdd3 --- /dev/null +++ b/app/src/main/java/com/sun/weather/ui/home/HomeFragment.kt @@ -0,0 +1,107 @@ +package com.sun.weather.ui.home + +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import com.bumptech.glide.Glide +import com.sun.weather.R +import com.sun.weather.base.BaseFragment +import com.sun.weather.data.model.entity.WeatherEntity +import com.sun.weather.databinding.FragmentHomeBinding +import com.sun.weather.ui.SharedViewModel +import org.koin.androidx.viewmodel.ext.android.activityViewModel +import org.koin.androidx.viewmodel.ext.android.viewModel +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import kotlin.math.roundToInt + +class HomeFragment : BaseFragment(FragmentHomeBinding::inflate) { + override val viewModel: HomeViewModel by viewModel() + override val sharedViewModel: SharedViewModel by activityViewModel() + private var cityName: String? = null + private var latitude: Double = 0.0 + private var longitude: Double = 0.0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + latitude = it.getDouble(ARG_LATITUDE) + longitude = it.getDouble(ARG_LONGITUDE) + } + } + + override fun initView() { + binding.icLocation.setOnClickListener { + fetchWeatherData() + } + } + + override fun initData() { + fetchWeatherData() + sharedViewModel.isNetworkAvailable.observe(viewLifecycleOwner) { isAvailable -> + if (isAvailable) { + fetchWeatherData() + } + } + viewModel.currentWeather.observe(viewLifecycleOwner) { weather -> + weather?.let { + updateUIWithCurrentWeather(it) + } + } + viewModel.errorMessage.observe(viewLifecycleOwner) { errorMessage -> + showError(errorMessage) + } + } + + override fun bindData() { + // TODO LATER + } + + private fun fetchWeatherData() { + viewModel.fetchCurrentWeather(latitude, longitude) + } + + private fun updateUIWithCurrentWeather(currentWeather: WeatherEntity) { + Log.v("LCD", currentWeather.toString()) + binding.tvLocation.text = getString(R.string.city_name, currentWeather.city, currentWeather.country) + cityName = currentWeather.city + binding.tvCurrentDay.text = getString(R.string.today) + formatDate(currentWeather.timeZone!!) + binding.tvCurrentTemperature.text = "${currentWeather.weatherCurrent?.currentTemperature?.roundToInt()}°C" + binding.tvCurrentText.text = currentWeather.weatherCurrent?.weatherDescription + binding.tvCurrentPercentCloud.text = currentWeather.weatherCurrent?.windSpeed.toString() + binding.tvCurrentHumidity.text = currentWeather.weatherCurrent?.humidity.toString() + binding.tvCurrentPercentCloud1.text = currentWeather.weatherCurrent?.percentCloud.toString() + Glide.with(this).load(currentWeather.weatherCurrent?.iconWeather).into(binding.ivCurrentWeather) + } + + private fun showError(errorMessage: String) { + Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show() + } + + companion object { + const val SELECTED_LOCATION = "selected_location" + const val ARG_LATITUDE = "latitude" + const val ARG_LONGITUDE = "longitude" + + fun newInstance( + latitude: Double, + longitude: Double, + ): HomeFragment { + val fragment = HomeFragment() + val args = Bundle() + args.putDouble(ARG_LATITUDE, latitude) + args.putDouble(ARG_LONGITUDE, longitude) + fragment.arguments = args + return fragment + } + + private fun formatDate(timestamp: Long): String { + val date = Date(timestamp * SECOND_TO_MILLIS) + return SimpleDateFormat(DATE_PATTERN, Locale.getDefault()).format(date) + } + + private const val SECOND_TO_MILLIS = 1000 + private const val DATE_PATTERN = ", dd MMMM" + } +} diff --git a/app/src/main/java/com/sun/weather/ui/home/HomeViewModel.kt b/app/src/main/java/com/sun/weather/ui/home/HomeViewModel.kt new file mode 100644 index 0000000..c96b759 --- /dev/null +++ b/app/src/main/java/com/sun/weather/ui/home/HomeViewModel.kt @@ -0,0 +1,39 @@ +package com.sun.weather.ui.home + +import android.util.Log +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.weather.utils.livedata.SingleLiveData +import com.sun.weather.data.model.entity.WeatherEntity +import com.sun.weather.data.repository.WeatherRepository +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.singleOrNull +import kotlinx.coroutines.launch + +class HomeViewModel( + private val weatherRepository: WeatherRepository, +) : ViewModel() { + var currentWeather = SingleLiveData() + var isLoading = MutableLiveData() + var errorMessage = MutableLiveData() + + fun fetchCurrentWeather( + latitude: Double, + longitude: Double, + ) { + isLoading.value = true + viewModelScope.launch { + try { + val weatherDeferred = + async { weatherRepository.getCurrentLocationWeather(latitude, longitude, "vi") } + currentWeather.postValue(weatherDeferred.await().singleOrNull()) + isLoading.postValue(false) + } catch (e: Exception) { + Log.v("LCD", "Tai View Model:\n" + e.message.toString()) + errorMessage.postValue(e.message) + isLoading.postValue(false) + } + } + } +} diff --git a/app/src/main/java/com/sun/weather/utils/listener/OnFetchListener.kt b/app/src/main/java/com/sun/weather/utils/listener/OnFetchListener.kt deleted file mode 100644 index 96db1a5..0000000 --- a/app/src/main/java/com/sun/weather/utils/listener/OnFetchListener.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.weather.utils.listener - -import android.location.Location - -interface OnFetchListener { - fun onDataLocation(location: Location?) -} diff --git a/app/src/main/java/com/sun/weather/utils/listener/OnItemClickListener.kt b/app/src/main/java/com/sun/weather/utils/listener/OnItemClickListener.kt index 2356f7b..c69090f 100644 --- a/app/src/main/java/com/sun/weather/utils/listener/OnItemClickListener.kt +++ b/app/src/main/java/com/sun/weather/utils/listener/OnItemClickListener.kt @@ -1,5 +1,4 @@ -package com.example.weather.utils.listener - +package com.sun.weather.utils.listener import android.view.View interface OnItemClickListener {