diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index 79ccf61..b268ef3 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,14 +4,6 @@
-
-
-
-
-
-
-
-
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 74dd639..b2c751a 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,3 @@
-
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9e90481..0525c5c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,11 +6,12 @@
+ android:theme="@style/Theme.WeatherDemo.NoActionBar"
+ android:windowSoftInputMode="adjustResize|stateHidden"/>
+
+
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..7c755a9
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/com/example/weatherdemo/data/api/WeatherApiService.kt b/app/src/main/java/com/example/weatherdemo/data/api/WeatherApiService.kt
index aa943e0..23e2ef4 100644
--- a/app/src/main/java/com/example/weatherdemo/data/api/WeatherApiService.kt
+++ b/app/src/main/java/com/example/weatherdemo/data/api/WeatherApiService.kt
@@ -1,22 +1,19 @@
package com.example.weatherdemo.data.api
+import com.example.weatherdemo.data.models.WeatherForecastResponse
import com.example.weatherdemo.data.models.WeatherResponse
+import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query
-import retrofit2.Response
-
/**
- * Retrofit service interface for Weather API
+ * Retrofit service interface for Weather API (WeatherAPI.com)
* Defines the API endpoints and request methods
*/
interface WeatherApiService {
/**
* Fetches current weather data for a specific location
- * @param key API key for authentication
- * @param query Location query (city name, zip code, etc.)
- * @param aqi Whether to include air quality index (yes/no)
*/
@GET("current.json")
suspend fun getCurrentWeather(
@@ -24,4 +21,17 @@ interface WeatherApiService {
@Query("q") query: String,
@Query("aqi") aqi: String = "yes"
): Response
-}
\ No newline at end of file
+
+ /**
+ * Fetches forecast (current + hourly + daily) for a specific location
+ * @param days number of days forecast
+ */
+ @GET("forecast.json")
+ suspend fun getForecastWeather(
+ @Query("key") key: String,
+ @Query("q") query: String,
+ @Query("days") days: Int = 3,
+ @Query("aqi") aqi: String = "no",
+ @Query("alerts") alerts: String = "no"
+ ): Response
+}
diff --git a/app/src/main/java/com/example/weatherdemo/data/models/WeatherForecastResponse.kt b/app/src/main/java/com/example/weatherdemo/data/models/WeatherForecastResponse.kt
new file mode 100644
index 0000000..b368e1d
--- /dev/null
+++ b/app/src/main/java/com/example/weatherdemo/data/models/WeatherForecastResponse.kt
@@ -0,0 +1,36 @@
+package com.example.weatherdemo.data.models
+
+/**
+ * Response model for WeatherAPI "forecast.json".
+ * This includes current weather + hourly & daily forecast.
+ */
+data class WeatherForecastResponse(
+ val location: Location,
+ val current: Current,
+ val forecast: Forecast
+)
+
+data class Forecast(
+ val forecastday: List
+)
+
+data class ForecastDay(
+ val date: String,
+ val day: Day,
+ val hour: List
+)
+
+data class Day(
+ val maxtemp_c: Double,
+ val mintemp_c: Double,
+ val avgtemp_c: Double,
+ val condition: Condition,
+ val daily_chance_of_rain: Int? = null
+)
+
+data class Hour(
+ val time: String,
+ val temp_c: Double,
+ val condition: Condition,
+ val chance_of_rain: Int? = null
+)
diff --git a/app/src/main/java/com/example/weatherdemo/data/repository/WeatherRepository.kt b/app/src/main/java/com/example/weatherdemo/data/repository/WeatherRepository.kt
index 7b06255..66fc5f6 100644
--- a/app/src/main/java/com/example/weatherdemo/data/repository/WeatherRepository.kt
+++ b/app/src/main/java/com/example/weatherdemo/data/repository/WeatherRepository.kt
@@ -1,7 +1,7 @@
package com.example.weatherdemo.data.repository
import com.example.weatherdemo.data.api.WeatherApiService
-import com.example.weatherdemo.data.models.WeatherResponse
+import com.example.weatherdemo.data.models.WeatherForecastResponse
import com.example.weatherdemo.utils.Result
import retrofit2.Response
@@ -12,23 +12,24 @@ import retrofit2.Response
class WeatherRepository(private val apiService: WeatherApiService) {
/**
- * Fetches weather data from API and returns a Result wrapper
- * @param location The location to get weather for
- * @return Result object containing either success or error
+ * Fetches forecast weather data from API and returns a Result wrapper.
+ * This uses WeatherAPI "forecast.json" so we can show hourly/daily prediction on main page.
+ *
+ * @param location The location to get weather for (city name, zip, lat/long)
+ * @param days number of forecast days (default 3)
*/
- suspend fun getWeatherData(location: String): Result {
+ suspend fun getWeatherForecast(location: String, days: Int = 3): Result {
return try {
- // Make API call
- val response: Response = apiService.getCurrentWeather(
+ val response: Response = apiService.getForecastWeather(
key = "822615b3cef1437bb0202739251712", // API key
- query = location
+ query = location,
+ days = days
)
- // Check if response is successful
if (response.isSuccessful) {
- val weatherResponse = response.body()
- if (weatherResponse != null) {
- Result.Success(weatherResponse)
+ val body = response.body()
+ if (body != null) {
+ Result.Success(body)
} else {
Result.Error(Exception("Empty response body"))
}
diff --git a/app/src/main/java/com/example/weatherdemo/ui/ChatActivity.kt b/app/src/main/java/com/example/weatherdemo/ui/ChatActivity.kt
index 35e3e3b..66cfcb1 100644
--- a/app/src/main/java/com/example/weatherdemo/ui/ChatActivity.kt
+++ b/app/src/main/java/com/example/weatherdemo/ui/ChatActivity.kt
@@ -3,8 +3,12 @@ package com.example.weatherdemo.ui
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
+import android.widget.TextView
+import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.widget.Toolbar
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.weatherdemo.R
@@ -20,64 +24,67 @@ class ChatActivity : AppCompatActivity() {
private lateinit var btnSend: Button
private lateinit var recyclerView: RecyclerView
private lateinit var chatAdapter: ChatAdapter
- private lateinit var username: String // username user aktif
+ private lateinit var username: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+
+ // ✅ penting: kita handle insets sendiri biar semua HP aman
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
setContentView(R.layout.activity_chat)
- // Toolbar + tombol back
- val toolbar = findViewById(R.id.toolbarChat)
- setSupportActionBar(toolbar)
- toolbar.setNavigationOnClickListener { finish() }
-
- // ✅ Ambil username dari Intent
username = intent.getStringExtra("username") ?: "Guest"
+ findViewById(R.id.btnBack).setOnClickListener { finish() }
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() { finish() }
+ })
+
+ val root = findViewById(R.id.chatRoot)
+
+ // ✅ INI INTI FIX: kalau keyboard muncul, kasih padding bawah sebesar tinggi keyboard
+ ViewCompat.setOnApplyWindowInsetsListener(root) { v, insets ->
+ val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
+ val sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+
+ // padding atas biar tidak nabrak status bar, padding bawah ikut keyboard
+ v.setPadding(
+ v.paddingLeft,
+ sysBars.top,
+ v.paddingRight,
+ ime.bottom
+ )
+ insets
+ }
+
etMessage = findViewById(R.id.etMessage)
btnSend = findViewById(R.id.btnSend)
recyclerView = findViewById(R.id.recyclerView)
- val layoutManager = LinearLayoutManager(this).apply {
- stackFromEnd = true
- }
- recyclerView.layoutManager = layoutManager
-
- // 🔥 WAJIB kirim username ke ChatAdapter
+ recyclerView.layoutManager = LinearLayoutManager(this).apply { stackFromEnd = true }
chatAdapter = ChatAdapter(username)
recyclerView.adapter = chatAdapter
btnSend.setOnClickListener {
val messageText = etMessage.text.toString().trim()
-
if (messageText.isNotEmpty()) {
-
- val time = SimpleDateFormat(
- "HH:mm",
- Locale.getDefault()
- ).format(Date())
-
- FirebaseUtils.sendMessage(
- username = username,
- message = messageText,
- time = time
- )
-
+ val time = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date())
+ FirebaseUtils.sendMessage(username = username, message = messageText, time = time)
etMessage.setText("")
}
}
+
+ // biar insets langsung diterapkan
+ ViewCompat.requestApplyInsets(root)
}
override fun onStart() {
super.onStart()
-
FirebaseUtils.getMessages { messages ->
chatAdapter.submitList(messages)
-
if (messages.isNotEmpty()) {
- recyclerView.post {
- recyclerView.scrollToPosition(messages.size - 1)
- }
+ recyclerView.post { recyclerView.scrollToPosition(messages.size - 1) }
}
}
}
diff --git a/app/src/main/java/com/example/weatherdemo/ui/LoginActivity.kt b/app/src/main/java/com/example/weatherdemo/ui/LoginActivity.kt
index 82228af..b040057 100644
--- a/app/src/main/java/com/example/weatherdemo/ui/LoginActivity.kt
+++ b/app/src/main/java/com/example/weatherdemo/ui/LoginActivity.kt
@@ -2,38 +2,33 @@ package com.example.weatherdemo.ui
import android.content.Intent
import android.os.Bundle
-import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.weatherdemo.R
+import com.google.android.material.button.MaterialButton
class LoginActivity : AppCompatActivity() {
- private lateinit var etUsername: EditText
- private lateinit var btnLogin: Button
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
- etUsername = findViewById(R.id.etUsername)
- btnLogin = findViewById(R.id.btnLogin)
+ val etUsername = findViewById(R.id.etUsername)
+ val btnLogin = findViewById(R.id.btnLogin)
btnLogin.setOnClickListener {
val username = etUsername.text.toString().trim()
- if (username.isNotEmpty()) {
- val intent = Intent(this, MainActivity::class.java)
-
- // ✅ FIX: key KONSISTEN
- intent.putExtra("username", username)
-
- startActivity(intent)
- finish()
- } else {
- Toast.makeText(this, "Please enter a username", Toast.LENGTH_SHORT).show()
+ if (username.isEmpty()) {
+ Toast.makeText(this, "Masukkan username dulu", Toast.LENGTH_SHORT).show()
+ return@setOnClickListener
}
+
+ val intent = Intent(this, MainActivity::class.java)
+ intent.putExtra("username", username)
+ startActivity(intent)
+ finish()
}
}
}
diff --git a/app/src/main/java/com/example/weatherdemo/ui/MainActivity.kt b/app/src/main/java/com/example/weatherdemo/ui/MainActivity.kt
index db8f87b..2465c3e 100644
--- a/app/src/main/java/com/example/weatherdemo/ui/MainActivity.kt
+++ b/app/src/main/java/com/example/weatherdemo/ui/MainActivity.kt
@@ -6,23 +6,25 @@ import android.os.Bundle
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
-import android.widget.LinearLayout
+import android.widget.ImageButton
+import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
-import androidx.core.content.ContextCompat
-import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
import com.example.weatherdemo.R
import com.example.weatherdemo.data.api.RetrofitInstance
-import com.example.weatherdemo.data.models.WeatherResponse
+import com.example.weatherdemo.data.models.WeatherForecastResponse
import com.example.weatherdemo.data.repository.WeatherRepository
+import com.example.weatherdemo.ui.adapter.DailyForecastAdapter
+import com.example.weatherdemo.ui.adapter.HourlyForecastAdapter
import com.example.weatherdemo.viewmodel.WeatherViewModel
import com.example.weatherdemo.viewmodel.WeatherViewModelFactory
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
-import com.google.android.material.floatingactionbutton.FloatingActionButton
class MainActivity : AppCompatActivity() {
@@ -30,26 +32,43 @@ class MainActivity : AppCompatActivity() {
WeatherViewModelFactory(WeatherRepository(RetrofitInstance.apiService))
}
+ private lateinit var hourlyAdapter: HourlyForecastAdapter
+ private lateinit var dailyAdapter: DailyForecastAdapter
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
- // ✅ FIX DI SINI SAJA
val username = intent.getStringExtra("username") ?: "Guest"
+ findViewById(R.id.tvWelcomeMessage).text = "Welcome, $username!"
- val tvWelcomeMessage = findViewById(R.id.tvWelcomeMessage)
- tvWelcomeMessage.text = "Welcome, $username!"
-
- // FAB chat → kirim username ke ChatActivity
- findViewById(R.id.fabChat).setOnClickListener {
+ // ✅ CHAT GLOBAL → ImageButton (BUKAN FloatingActionButton)
+ findViewById(R.id.fabChat).setOnClickListener {
val i = Intent(this, ChatActivity::class.java)
i.putExtra("username", username)
startActivity(i)
}
+ initForecastLists()
initViews()
setupObservers()
- viewModel.fetchWeatherData("London")
+
+ // default city (AMAN)
+ viewModel.fetchForecast("Bekasi", days = 3)
+ }
+
+ private fun initForecastLists() {
+ hourlyAdapter = HourlyForecastAdapter()
+ findViewById(R.id.rvHourlyForecast).apply {
+ layoutManager = LinearLayoutManager(this@MainActivity, LinearLayoutManager.HORIZONTAL, false)
+ adapter = hourlyAdapter
+ }
+
+ dailyAdapter = DailyForecastAdapter()
+ findViewById(R.id.rvDailyForecast).apply {
+ layoutManager = LinearLayoutManager(this@MainActivity)
+ adapter = dailyAdapter
+ }
}
private fun initViews() {
@@ -60,7 +79,7 @@ class MainActivity : AppCompatActivity() {
val location = etSearch.text.toString().trim()
if (location.isNotEmpty()) {
hideKeyboard()
- viewModel.fetchWeatherData(location)
+ viewModel.fetchForecast(location, days = 3)
} else {
Toast.makeText(this, "Please enter a location", Toast.LENGTH_SHORT).show()
}
@@ -68,78 +87,73 @@ class MainActivity : AppCompatActivity() {
}
private fun setupObservers() {
- viewModel.weatherData.observe(this, Observer { weatherData ->
- weatherData?.let { updateWeatherUI(it) }
- })
+ viewModel.forecastData.observe(this) { forecast ->
+ forecast?.let { updateWeatherUI(it) }
+ }
- viewModel.isLoading.observe(this, Observer { isLoading ->
+ viewModel.isLoading.observe(this) { isLoading ->
findViewById(R.id.progressBar).visibility =
if (isLoading) View.VISIBLE else View.GONE
- })
+ }
- viewModel.errorMessage.observe(this, Observer { errorMessage ->
- val tvError = findViewById(R.id.tvError)
- if (errorMessage.isNotEmpty()) {
- tvError.text = errorMessage
- tvError.visibility = View.VISIBLE
- findViewById(R.id.weatherCard).visibility = View.GONE
- Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show()
- } else {
- tvError.visibility = View.GONE
- }
- })
- }
-
- private fun updateWeatherUI(weatherResponse: WeatherResponse) {
- val weatherCard = findViewById(R.id.weatherCard)
- val tvLocation = findViewById(R.id.tvLocation)
- val tvTemperature = findViewById(R.id.tvTemperature)
- val tvCondition = findViewById(R.id.tvCondition)
- val tvFeelsLike = findViewById(R.id.tvFeelsLike)
- val tvHumidity = findViewById(R.id.tvHumidity)
- val tvWind = findViewById(R.id.tvWind)
-
- tvLocation.text = "${weatherResponse.location.name}, ${weatherResponse.location.country}"
- tvTemperature.text = "${weatherResponse.current.temp_c}°C"
- tvCondition.text = weatherResponse.current.condition.text
- tvFeelsLike.text = "${weatherResponse.current.feelslike_c}°C"
- tvHumidity.text = "${weatherResponse.current.humidity}%"
- tvWind.text = "${weatherResponse.current.wind_kph} km/h"
-
- val condition = weatherResponse.current.condition.text.lowercase()
- val mainLayout = findViewById(R.id.mainLayout)
-
- when {
- condition.contains("clear") || condition.contains("sunny") -> {
- weatherCard.setCardBackgroundColor(ContextCompat.getColor(this, R.color.sunny))
- mainLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.sunny_background))
- }
- condition.contains("rain") -> {
- weatherCard.setCardBackgroundColor(ContextCompat.getColor(this, R.color.rainy))
- mainLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.rainy_background))
- }
- condition.contains("cloudy") -> {
- weatherCard.setCardBackgroundColor(ContextCompat.getColor(this, R.color.cloudy))
- mainLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.cloudy_background))
- }
- condition.contains("snow") -> {
- weatherCard.setCardBackgroundColor(ContextCompat.getColor(this, R.color.snowy))
- mainLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.snowy_background))
- }
- else -> {
- weatherCard.setCardBackgroundColor(ContextCompat.getColor(this, R.color.default_weather))
- mainLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.default_weather_background))
+ viewModel.errorMessage.observe(this) { errorMessage ->
+ findViewById(R.id.tvError).apply {
+ text = errorMessage
+ visibility = if (errorMessage.isNullOrBlank()) View.GONE else View.VISIBLE
}
}
+ }
+
+ private fun updateWeatherUI(weather: WeatherForecastResponse) {
+ val weatherCard = findViewById(R.id.weatherCard)
+ val ivBg = findViewById(R.id.ivBg)
+
+ findViewById(R.id.tvLocation).text =
+ "${weather.location.name}, ${weather.location.country}"
+ findViewById(R.id.tvTemperature).text =
+ "${weather.current.temp_c}°C"
+ findViewById(R.id.tvCondition).text =
+ weather.current.condition.text
+ findViewById(R.id.tvFeelsLike).text =
+ "${weather.current.feelslike_c}°C"
+ findViewById(R.id.tvHumidity).text =
+ "${weather.current.humidity}%"
+ findViewById(R.id.tvWind).text =
+ "${weather.current.wind_kph} km/h"
+
+ // ✅ BACKGROUND FIX (no typo)
+ val condition = weather.current.condition.text.lowercase()
+ when {
+ condition.contains("sunny") || condition.contains("clear") ->
+ ivBg.setImageResource(R.drawable.sunny)
+
+ condition.contains("rain") || condition.contains("drizzle") || condition.contains("thunder") ->
+ ivBg.setImageResource(R.drawable.rainy)
+
+ condition.contains("cloud") || condition.contains("overcast") || condition.contains("mist") || condition.contains("fog") ->
+ ivBg.setImageResource(R.drawable.cloudy)
+
+ condition.contains("snow") || condition.contains("sleet") || condition.contains("blizzard") ->
+ ivBg.setImageResource(R.drawable.snowy)
+
+ else ->
+ ivBg.setImageResource(R.drawable.defaultt)
+ }
+
+ // forecast
+ val hourly = weather.forecast.forecastday.firstOrNull()?.hour.orEmpty().take(12)
+ hourlyAdapter.submitList(hourly)
+
+ dailyAdapter.submitList(weather.forecast.forecastday)
weatherCard.visibility = View.VISIBLE
}
private fun hideKeyboard() {
- val view = this.currentFocus
- view?.let {
+ currentFocus?.let {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(it.windowToken, 0)
+ it.clearFocus()
}
}
}
diff --git a/app/src/main/java/com/example/weatherdemo/ui/adapter/DailyForecastAdapter.kt b/app/src/main/java/com/example/weatherdemo/ui/adapter/DailyForecastAdapter.kt
new file mode 100644
index 0000000..b125ee5
--- /dev/null
+++ b/app/src/main/java/com/example/weatherdemo/ui/adapter/DailyForecastAdapter.kt
@@ -0,0 +1,43 @@
+package com.example.weatherdemo.ui.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.example.weatherdemo.R
+import com.example.weatherdemo.data.models.ForecastDay
+
+class DailyForecastAdapter(
+ private var items: List = emptyList()
+) : RecyclerView.Adapter() {
+
+ fun submitList(newItems: List) {
+ items = newItems
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DayVH {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_day_forecast, parent, false)
+ return DayVH(view)
+ }
+
+ override fun onBindViewHolder(holder: DayVH, position: Int) {
+ holder.bind(items[position])
+ }
+
+ override fun getItemCount(): Int = items.size
+
+ class DayVH(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val tvDate: TextView = itemView.findViewById(R.id.tvDate)
+ private val tvRange: TextView = itemView.findViewById(R.id.tvTempRange)
+ private val tvCondition: TextView = itemView.findViewById(R.id.tvDayCondition)
+
+ fun bind(item: ForecastDay) {
+ tvDate.text = item.date
+ tvRange.text = "${item.day.mintemp_c}°C - ${item.day.maxtemp_c}°C"
+ tvCondition.text = item.day.condition.text
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/weatherdemo/ui/adapter/HourlyForecastAdapter.kt b/app/src/main/java/com/example/weatherdemo/ui/adapter/HourlyForecastAdapter.kt
new file mode 100644
index 0000000..52610e3
--- /dev/null
+++ b/app/src/main/java/com/example/weatherdemo/ui/adapter/HourlyForecastAdapter.kt
@@ -0,0 +1,45 @@
+package com.example.weatherdemo.ui.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.example.weatherdemo.R
+import com.example.weatherdemo.data.models.Hour
+
+class HourlyForecastAdapter(
+ private var items: List = emptyList()
+) : RecyclerView.Adapter() {
+
+ fun submitList(newItems: List) {
+ items = newItems
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HourVH {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_hour_forecast, parent, false)
+ return HourVH(view)
+ }
+
+ override fun onBindViewHolder(holder: HourVH, position: Int) {
+ holder.bind(items[position])
+ }
+
+ override fun getItemCount(): Int = items.size
+
+ class HourVH(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val tvHour: TextView = itemView.findViewById(R.id.tvHour)
+ private val tvTemp: TextView = itemView.findViewById(R.id.tvHourTemp)
+ private val tvCondition: TextView = itemView.findViewById(R.id.tvHourCondition)
+
+ fun bind(item: Hour) {
+ // item.time example: "2025-12-29 13:00"
+ val hourText = item.time.substringAfter(" ").ifBlank { item.time }
+ tvHour.text = hourText
+ tvTemp.text = "${item.temp_c}°C"
+ tvCondition.text = item.condition.text
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/weatherdemo/viewmodel/WeatherViewModel.kt b/app/src/main/java/com/example/weatherdemo/viewmodel/WeatherViewModel.kt
index 81e5f11..f23f389 100644
--- a/app/src/main/java/com/example/weatherdemo/viewmodel/WeatherViewModel.kt
+++ b/app/src/main/java/com/example/weatherdemo/viewmodel/WeatherViewModel.kt
@@ -4,10 +4,10 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.example.weatherdemo.data.models.WeatherResponse
+import com.example.weatherdemo.data.models.WeatherForecastResponse
import com.example.weatherdemo.data.repository.WeatherRepository
-import kotlinx.coroutines.launch
import com.example.weatherdemo.utils.Result
+import kotlinx.coroutines.launch
/**
* ViewModel class that prepares and manages UI-related data
@@ -16,9 +16,9 @@ import com.example.weatherdemo.utils.Result
*/
class WeatherViewModel(private val repository: WeatherRepository) : ViewModel() {
- // LiveData for weather information - observed by UI
- private val _weatherData = MutableLiveData()
- val weatherData: LiveData = _weatherData
+ // LiveData for forecast response (current + hourly + daily)
+ private val _forecastData = MutableLiveData()
+ val forecastData: LiveData = _forecastData
// LiveData for loading state - to show/hide progress bar
private val _isLoading = MutableLiveData()
@@ -29,17 +29,15 @@ class WeatherViewModel(private val repository: WeatherRepository) : ViewModel()
val errorMessage: LiveData = _errorMessage
/**
- * Fetches weather data for a given location
- * Uses coroutines for background operations
- * @param location The city name to search for
+ * Fetches forecast data for a given location
*/
- fun fetchWeatherData(location: String) {
+ fun fetchForecast(location: String, days: Int = 3) {
_isLoading.value = true
viewModelScope.launch {
- when (val result = repository.getWeatherData(location)) {
+ when (val result = repository.getWeatherForecast(location, days)) {
is Result.Success -> {
- _weatherData.value = result.data
+ _forecastData.value = result.data
_errorMessage.value = ""
}
is Result.Error -> {
@@ -49,4 +47,4 @@ class WeatherViewModel(private val repository: WeatherRepository) : ViewModel()
_isLoading.value = false
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/res/drawable/ic_weatherhub.webp b/app/src/main/res/drawable/ic_weatherhub.webp
new file mode 100644
index 0000000..1efb2e5
Binary files /dev/null and b/app/src/main/res/drawable/ic_weatherhub.webp differ
diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml
index 58c12cb..77eafa4 100644
--- a/app/src/main/res/layout/activity_chat.xml
+++ b/app/src/main/res/layout/activity_chat.xml
@@ -1,53 +1,90 @@
-
+ android:layout_height="match_parent">
-
-
+
+ android:background="#2F55F4"
+ android:paddingStart="12dp"
+ android:paddingEnd="12dp"
+ android:gravity="center_vertical"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent">
-
+
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/topBar"
+ app:layout_constraintBottom_toTopOf="@id/inputBar"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
-
-
+
+ android:orientation="horizontal"
+ android:padding="8dp"
+ android:background="#F2F2F2"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent">
-
-
+
-
+
+
+
+
diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml
index a858474..eeb6050 100644
--- a/app/src/main/res/layout/activity_login.xml
+++ b/app/src/main/res/layout/activity_login.xml
@@ -1,28 +1,95 @@
-
+ android:background="@android:color/white">
-
-
+
+
+ android:text="WeatherHub"
+ android:textStyle="bold"
+ android:textSize="22sp"
+ android:textColor="@android:color/black"
+ app:layout_constraintTop_toBottomOf="@id/ivLogo"
+ app:layout_constraintBottom_toTopOf="@id/cardUsername"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:layout_marginBottom="44dp"/>
-
-
+ android:textAllCaps="false"
+ android:textSize="16sp"
+ android:textColor="@android:color/white"
+ app:cornerRadius="28dp"
+ app:elevation="10dp"
+ app:backgroundTint="#2F55F4"
+ app:layout_constraintTop_toBottomOf="@id/cardUsername"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:layout_marginBottom="90dp"/>
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index cfcfb24..e6d397d 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -2,196 +2,325 @@
+ android:layout_height="match_parent">
+
+
+
+
+ android:fillViewport="true"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp"
+ android:paddingStart="12dp"
+ android:paddingEnd="12dp"
+ android:focusable="true"
+ android:focusableInTouchMode="true">
+ android:orientation="vertical">
-
-
+
+ app:cardUseCompatPadding="false"
+ app:strokeWidth="0dp"
+ app:cardBackgroundColor="#2F55F4">
-
+
+ android:layout_marginBottom="14dp"
+ app:cardCornerRadius="18dp"
+ app:cardElevation="10dp"
+ app:cardUseCompatPadding="false"
+ app:strokeWidth="1dp"
+ app:strokeColor="#22FFFFFF"
+ app:cardBackgroundColor="#55FFFFFF">
+ android:padding="14dp"
+ android:gravity="center_vertical">
+ android:textColor="@android:color/white"
+ android:textColorHint="#CCFFFFFF"
+ android:background="@android:color/transparent"
+ android:gravity="center_vertical"
+ android:paddingStart="12dp"
+ android:paddingEnd="12dp" />
+ app:iconPadding="8dp"
+ app:backgroundTint="#2F55F4" />
-
+ android:visibility="gone"
+ android:layout_marginBottom="10dp" />
-
+ android:layout_marginBottom="10dp" />
-
+
+ app:cardCornerRadius="22dp"
+ app:cardElevation="0dp"
+ app:cardUseCompatPadding="false"
+ app:strokeWidth="0dp"
+ app:cardBackgroundColor="@android:color/transparent">
+ android:orientation="vertical">
-
+
+ app:cardCornerRadius="18dp"
+ app:cardElevation="10dp"
+ app:cardUseCompatPadding="false"
+ app:strokeWidth="1dp"
+ app:strokeColor="#22FFFFFF"
+ app:cardBackgroundColor="#26FFFFFF">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ app:cardCornerRadius="18dp"
+ app:cardElevation="2dp"
+ app:cardUseCompatPadding="false"
+ app:strokeWidth="1dp"
+ app:strokeColor="#22FFFFFF"
+ app:cardBackgroundColor="#26FFFFFF">
-
+
+
+
+
+
+
+
+
+
+
+
+ app:cardCornerRadius="18dp"
+ app:cardElevation="2dp"
+ app:cardUseCompatPadding="false"
+ app:strokeWidth="1dp"
+ app:strokeColor="#22FFFFFF"
+ app:cardBackgroundColor="#26FFFFFF">
-
-
-
+ android:orientation="vertical"
+ android:padding="16dp">
-
+
-
-
-
-
-
-
-
-
+
+
+
@@ -199,15 +328,16 @@
-
-
+
+ android:layout_margin="18dp"
+ android:background="@android:color/transparent"
+ android:src="@drawable/logo_chat"
+ android:scaleType="fitCenter"
+ android:contentDescription="Chat Global" />
diff --git a/app/src/main/res/layout/item_day_forecast.xml b/app/src/main/res/layout/item_day_forecast.xml
new file mode 100644
index 0000000..fa86050
--- /dev/null
+++ b/app/src/main/res/layout/item_day_forecast.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_hour_forecast.xml b/app/src/main/res/layout/item_hour_forecast.xml
new file mode 100644
index 0000000..d612a51
--- /dev/null
+++ b/app/src/main/res/layout/item_hour_forecast.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index c4a603d..b81fa46 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index c4a603d..b81fa46 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
index d7c3f94..76c614d 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
index 2eea385..fa505e9 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
index 2da0aba..c7c7a81 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_weatherhub.webp b/app/src/main/res/mipmap-hdpi/ic_weatherhub.webp
new file mode 100644
index 0000000..fa505e9
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_weatherhub.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
index 4cd3f8e..61b05fe 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
index 250fcfa..515c1e1 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
index dd770c0..ad9270e 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_weatherhub.webp b/app/src/main/res/mipmap-mdpi/ic_weatherhub.webp
new file mode 100644
index 0000000..515c1e1
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_weatherhub.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
index 36d9900..aec55d6 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
index b2cdb33..f3ed7c9 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
index 69d5e91..1c1bc29 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_weatherhub.webp b/app/src/main/res/mipmap-xhdpi/ic_weatherhub.webp
new file mode 100644
index 0000000..f3ed7c9
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_weatherhub.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
index 4b07d8e..d35ea4b 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
index b66b2af..4fd1527 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
index 5df08c4..d10c75f 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_weatherhub.webp b/app/src/main/res/mipmap-xxhdpi/ic_weatherhub.webp
new file mode 100644
index 0000000..4fd1527
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_weatherhub.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
index 47e8554..5e3c6cf 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
index e9087cb..1efb2e5 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
index 3579617..e39312b 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_weatherhub.webp b/app/src/main/res/mipmap-xxxhdpi/ic_weatherhub.webp
new file mode 100644
index 0000000..1efb2e5
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_weatherhub.webp differ
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 58bb8d2..31ccc29 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -6,4 +6,7 @@
+
+
\ No newline at end of file