viewBinding
findViewById를 쓰지 않고, XML의 view component 에 접근하는 object를 반환받아 view에 접근하는 방식
https://developer.android.com/topic/libraries/view-binding?hl=ko
🚨 장점
1. findViewById()를 사용하지 않아도 된다. 자동으로 xml에서 만든 View들을 만들어준다.
2. Null 안정성 - 개발자가 실수로 유효하지 않은 id를 사용할 때 발생하는 null 오류를 방지할 수 있다
3. Type 안정성 - TextView의 타입을 ImageView라고 잘못 적어서 발생하는 cast exception 을 방지할 수 있다.
4. findViewById 를 사용했을 때 보다 속도가 상대적으로 빠르다.
🚨 사용 방법
build.gradle(:app)
viewBinding 의 enabled 를 true 로 설정합니다. 안드로이드 스튜디오의 버전에 따라 표시방법이 다릅니다.
// 안드로이드 스튜디오 3.6 ~ 4.0
android {
...
viewBinding {
enabled = true
}
}
// 안드로이드 스튜디오 4.0 이상
android {
...
buildFeatures {
viewBinding = true
}
}
액티비티 Activity
Kotlin 으로 작성하였습니다.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityWeatherBinding //activity_weather.xml 을 바인딩
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityWeatherBinding.inflate(layoutInflater)
setContentView(binding.root) //xml 전체를 감싸는 최상단 부모를 root 라는 property 로 제공
binding.textView.text = "텍스트"
}
...
}
프래그먼트 Fragment
프래그먼트는 액티비티와 다른 건 똑같은데 onDestroyView() 에서 binding 에 null 을 집어넣어 주는 것입니다.
프래그먼트는 뷰보다 더 오래 살아남아서 뷰가 제거될 때(onDestroyView) 이 바인딩 클래스의 인스턴스도 같이 비워줘야 합니다.
class TabFragment : Fragment() {
private var binding: FragmentTabBinding? = null //fragment_tab.xml 을 바인딩
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// return super.onCreateView(inflater, container, savedInstanceState)
binding = FragmentTabBinding.inflate(inflater, container, false)
binding?.textview?.text = "텍스트"
return binding?.root
}
...
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
}
xml 레이아웃 - tools:viewBindingIgnore
viewBinding 클래스는 레이아웃 별로 무조건 생성되기 때문에 viewBinding 클래스를 사용하지 않을 경우
tools:viewBindingIgnore 속성을 true 로 레이아웃 루트 뷰에 추가하여 줍니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:viewBindingIgnore="true"
tools:context=".view.MainActivity" >
...
</androidx.constraintlayout.widget.ConstraintLayout>
🚨 예제 코드
안드로이드에서 서버와 통신하기 위한 retrofit 과 viewBinding 을 이용하여 MVVM 패턴을 적용한 예제 코드를 openweathermap 의 api 를 통해 날씨 정보를 표시해주는 앱으로 만들어 보았습니다. 🌤
https://openweathermap.org/api
결과 화면을 먼저 보여드릴게요!
결과 화면
WeatherActivity 의 화면입니다.
하단 FloatingActionButton 에 open/close 효과를 추가하여 상세페이지로 이동하는 버튼, 현재 날씨를 공유하는 버튼을 만들었습니다.
DetailActivity 화면은 recyclerview 를 Grid 형태로 만들어서 4일간의 일기예보를 cardView 형태로 표시해주었습니다.
openweathermap 에서 제공하는 아이콘이 있지만 저는 drawable 에 아이콘들을 추가하여 사용하였습니다.
오류 발생 시 Dialog 를 표시하여 확인 버튼을 클릭 시 앱을 종료하도록 하였습니다.
작성한 파일 목록 입니다.
1. 초기 설정
1-1. AndroidManifest.xml
1-2. build.gradle(:app)
1-3. strings.xml / colors.xml
2. Model 생성
2-1. WeatherModel.kt
2-2. ForecastModel.kt
3. Network Service 생성
3-1. CookiesIntercepter
3-2. WeatherAPIService
3-3. WeatherAPIClient
3-4. RemoteDataSource
3-5. RemoteDataSourceImpl
4. Repository 생성
4-1. WeatherRepository.kt
5. ViewModel 생성
5-1. WeatherViewModel.kt
6. View 생성
6-1. WeatherActivity.kt
6-2. activity_weather.xml
6-3. DetailActivity.kt
6-4. activity_detail.xml
6-5. ListAdapter.kt
6-6. item_list.xml
6-7. CustomDIalog.kt
6-8. layout_dialog.xml
1. 초기 설정
1-1. AndroidManifest.xml
인터넷 권한 을 추가해야 합니다.
<uses-permission android:name="android.permission.INTERNET"/>
를 추가해주세요!
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.eun.myweatherkotlin_mvvm_01">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.MyWeatherKotlin_MVVM_01">
<activity android:name=".view.WeatherActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".view.DetailActivity">
</activity>
</application>
</manifest>
1-2. build.gradle(:app)
하단 dependencies {} 안에 gson, glide, retrofit 에 대한 라이브러리를 사용하기 위해 추가해 줍니다.
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.eun.myweatherkotlin_mvvm_01"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
viewBinding {
enabled = true
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
//gson
implementation 'com.google.code.gson:gson:2.8.6'
//glide
implementation 'com.github.bumptech.glide:glide:4.9.0'
//retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.4.1'
}
1-3. strings.xml
openweathermap.org 의 주소와 해당 사이트에서 발급받은 key값을 여기저기서 가져다 사용하기 위해 res/values/strings.xml 의 resources 에 지정해주었습니다.
<resources>
<string name="app_name">MyWeatherKotlin_MVVM_01</string>
<string name="app_title">MyWeather</string>
<string name="weather_url">http://api.openweathermap.org/</string>
<string name="weather_app_id">key값</string>
<string name="weather_seoul_id">1835848</string>
</resources>
1-4. colors.xml
안드로이드 Material Design에서 샘플로 나온 컬러를 hex값으로 변환한 값을 res/values/colors.xml 의 resources 에 지정해서 사용하였습니다.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Red -->
<color name="red_50">#FFEBEE</color>
<color name="red_100">#FFCDD2</color>
<color name="red_200">#EF9A9A</color>
<color name="red_300">#E57373</color>
<color name="red_400">#EF5350</color>
<color name="red_500">#F44336</color>
<color name="red_600">#E53935</color>
<color name="red_700">#D32F2F</color>
<color name="red_800">#C62828</color>
<color name="red_900">#B71C1C</color>
<color name="red_A100">#FF8A80</color>
<color name="red_A200">#FF5252</color>
<color name="red_A400">#FF1744</color>
<color name="red_A700">#D50000</color>
<!-- Red -->
<!-- Pink -->
<color name="pink_50">#FCE4EC</color>
<color name="pink_100">#F8BBD0</color>
<color name="pink_200">#F48FB1</color>
<color name="pink_300">#F06292</color>
<color name="pink_400">#EC407A</color>
<color name="pink_500">#E91E63</color>
<color name="pink_600">#D81B60</color>
<color name="pink_700">#C2185B</color>
<color name="pink_800">#AD1457</color>
<color name="pink_900">#880E4F</color>
<color name="pink_A100">#FF80AB</color>
<color name="pink_A200">#FF4081</color>
<color name="pink_A400">#F50057</color>
<color name="pink_A700">#C51162</color>
<!-- Pink -->
<!-- Purple -->
<color name="purple_50">#F3E5F5</color>
<color name="purple_100">#E1BEE7</color>
<color name="purple_200">#CE93D8</color>
<color name="purple_300">#BA68C8</color>
<color name="purple_400">#AB47BC</color>
<color name="purple_500">#9C27B0</color>
<color name="purple_600">#8E24AA</color>
<color name="purple_700">#7B1FA2</color>
<color name="purple_800">#6A1B9A</color>
<color name="purple_900">#4A148C</color>
<color name="purple_A100">#EA80FC</color>
<color name="purple_A200">#E040FB</color>
<color name="purple_A400">#D500F9</color>
<color name="purple_A700">#AA00FF</color>
<!-- Purple -->
<!-- Deep Purple -->
<color name="dark_purple_50">#EDE7F6</color>
<color name="dark_purple_100">#D1C4E9</color>
<color name="dark_purple_200">#B39DDB</color>
<color name="dark_purple_300">#9575CD</color>
<color name="dark_purple_400">#7E57C2</color>
<color name="dark_purple_500">#673AB7</color>
<color name="dark_purple_600">#5E35B1</color>
<color name="dark_purple_700">#512DA8</color>
<color name="dark_purple_800">#4527A0</color>
<color name="dark_purple_900">#311B92</color>
<color name="dark_purple_A100">#B388FF</color>
<color name="dark_purple_A200">#7C4DFF</color>
<color name="dark_purple_A400">#651FFF</color>
<color name="dark_purple_A700">#6200EA</color>
<!-- Deep Purple -->
<!-- Indigo -->
<color name="indigo_50">#E8EAF6</color>
<color name="indigo_100">#C5CAE9</color>
<color name="indigo_200">#9FA8DA</color>
<color name="indigo_300">#7986CB</color>
<color name="indigo_400">#5C6BC0</color>
<color name="indigo_500">#3F51B5</color>
<color name="indigo_600">#3949AB</color>
<color name="indigo_700">#303F9F</color>
<color name="indigo_800">#283593</color>
<color name="indigo_900">#1A237E</color>
<color name="indigo_A100">#8C9EFF</color>
<color name="indigo_A200">#536DFE</color>
<color name="indigo_A400">#3D5AFE</color>
<color name="indigo_A700">#304FFE</color>
<!-- Indigo -->
<!-- Blue -->
<color name="blue_50">#E3F2FD</color>
<color name="blue_100">#BBDEFB</color>
<color name="blue_200">#90CAF9</color>
<color name="blue_300">#64B5F6</color>
<color name="blue_400">#42A5F5</color>
<color name="blue_500">#2196F3</color>
<color name="blue_600">#1E88E5</color>
<color name="blue_700">#1976D2</color>
<color name="blue_800">#1565C0</color>
<color name="blue_900">#0D47A1</color>
<color name="blue_A100">#82B1FF</color>
<color name="blue_A200">#448AFF</color>
<color name="blue_A400">#2979FF</color>
<color name="blue_A700">#2962FF</color>
<!-- Blue -->
<!-- Light Blue -->
<color name="light_blue_50">#E1F5FE</color>
<color name="light_blue_100">#B3E5FC</color>
<color name="light_blue_200">#81D4FA</color>
<color name="light_blue_300">#4FC3F7</color>
<color name="light_blue_400">#29B6F6</color>
<color name="light_blue_500">#03A9F4</color>
<color name="light_blue_600">#039BE5</color>
<color name="light_blue_700">#0288D1</color>
<color name="light_blue_800">#0277BD</color>
<color name="light_blue_900">#01579B</color>
<color name="light_blue_A100">#80D8FF</color>
<color name="light_blue_A200">#40C4FF</color>
<color name="light_blue_A400">#00B0FF</color>
<color name="light_blue_A700">#0091EA</color>
<!-- Light Blue -->
<!-- Cyan -->
<color name="cyan_50">#E0F7FA</color>
<color name="cyan_100">#B2EBF2</color>
<color name="cyan_200">#80DEEA</color>
<color name="cyan_300">#4DD0E1</color>
<color name="cyan_400">#26C6DA</color>
<color name="cyan_500">#00BCD4</color>
<color name="cyan_600">#00ACC1</color>
<color name="cyan_700">#0097A7</color>
<color name="cyan_800">#00838F</color>
<color name="cyan_900">#006064</color>
<color name="cyan_A100">#84FFFF</color>
<color name="cyan_A200">#18FFFF</color>
<color name="cyan_A400">#00E5FF</color>
<color name="cyan_A700">#00B8D4</color>
<!-- Cyan -->
<!-- Teal -->
<color name="teal_50">#E0F2F1</color>
<color name="teal_100">#B2DFDB</color>
<color name="teal_200">#80CBC4</color>
<color name="teal_300">#4DB6AC</color>
<color name="teal_400">#26A69A</color>
<color name="teal_500">#009688</color>
<color name="teal_600">#00897B</color>
<color name="teal_700">#00796B</color>
<color name="teal_800">#00695C</color>
<color name="teal_900">#004D40</color>
<color name="teal_A100">#A7FFEB</color>
<color name="teal_A200">#64FFDA</color>
<color name="teal_A400">#1DE9B6</color>
<color name="teal_A700">#00BFA5</color>
<!-- Teal -->
<!-- Green -->
<color name="green_50">#E8F5E9</color>
<color name="green_100">#C8E6C9</color>
<color name="green_200">#A5D6A7</color>
<color name="green_300">#81C784</color>
<color name="green_400">#66BB6A</color>
<color name="green_500">#4CAF50</color>
<color name="green_600">#43A047</color>
<color name="green_700">#388E3C</color>
<color name="green_800">#2E7D32</color>
<color name="green_900">#1B5E20</color>
<color name="green_A100">#B9F6CA</color>
<color name="green_A200">#69F0AE</color>
<color name="green_A400">#00E676</color>
<color name="green_A700">#00C853</color>
<!-- Green -->
<!-- Light Green -->
<color name="light_green_50">#F1F8E9</color>
<color name="light_green_100">#DCEDC8</color>
<color name="light_green_200">#C5E1A5</color>
<color name="light_green_300">#AED581</color>
<color name="light_green_400">#9CCC65</color>
<color name="light_green_500">#8BC34A</color>
<color name="light_green_600">#7CB342</color>
<color name="light_green_700">#689F38</color>
<color name="light_green_800">#558B2F</color>
<color name="light_green_900">#33691E</color>
<color name="light_green_A100">#CCFF90</color>
<color name="light_green_A200">#B2FF59</color>
<color name="light_green_A400">#76FF03</color>
<color name="light_green_A700">#64DD17</color>
<!-- Light Green -->
<!-- Lime -->
<color name="lime_50">#F9FBE7</color>
<color name="lime_100">#F0F4C3</color>
<color name="lime_200">#E6EE9C</color>
<color name="lime_300">#DCE775</color>
<color name="lime_400">#D4E157</color>
<color name="lime_500">#CDDC39</color>
<color name="lime_600">#C0CA33</color>
<color name="lime_700">#AFB42B</color>
<color name="lime_800">#9E9D24</color>
<color name="lime_900">#827717</color>
<color name="lime_A100">#F4FF81</color>
<color name="lime_A200">#EEFF41</color>
<color name="lime_A400">#C6FF00</color>
<color name="lime_A700">#AEEA00</color>
<!-- Lime -->
<!-- Yellow -->
<color name="yellow_50">#FFFDE7</color>
<color name="yellow_100">#FFF9C4</color>
<color name="yellow_200">#FFF59D</color>
<color name="yellow_300">#FFF176</color>
<color name="yellow_400">#FFEE58</color>
<color name="yellow_500">#FFEB3B</color>
<color name="yellow_600">#FDD835</color>
<color name="yellow_700">#FBC02D</color>
<color name="yellow_800">#F9A825</color>
<color name="yellow_900">#F57F17</color>
<color name="yellow_A100">#FFFF8D</color>
<color name="yellow_A200">#FFFF00</color>
<color name="yellow_A400">#FFEA00</color>
<color name="yellow_A700">#FFD600</color>
<!-- Yellow -->
<!-- Amber -->
<color name="amber_50">#FFF8E1</color>
<color name="amber_100">#FFECB3</color>
<color name="amber_200">#FFE082</color>
<color name="amber_300">#FFD54F</color>
<color name="amber_400">#FFCA28</color>
<color name="amber_500">#FFC107</color>
<color name="amber_600">#FFB300</color>
<color name="amber_700">#FFA000</color>
<color name="amber_800">#FF8F00</color>
<color name="amber_900">#FF6F00</color>
<color name="amber_A100">#FFE57F</color>
<color name="amber_A200">#FFD740</color>
<color name="amber_A400">#FFC400</color>
<color name="amber_A700">#FFAB00</color>
<!-- Amber -->
<!-- Orange -->
<color name="orange_50">#FFF3E0</color>
<color name="orange_100">#FFE0B2</color>
<color name="orange_200">#FFCC80</color>
<color name="orange_300">#FFB74D</color>
<color name="orange_400">#FFA726</color>
<color name="orange_500">#FF9800</color>
<color name="orange_600">#FB8C00</color>
<color name="orange_700">#F57C00</color>
<color name="orange_800">#EF6C00</color>
<color name="orange_900">#E65100</color>
<color name="orange_A100">#FFD180</color>
<color name="orange_A200">#FFAB40</color>
<color name="orange_A400">#FF9100</color>
<color name="orange_A700">#FF6D00</color>
<!-- Orange -->
<!-- Deep Orange -->
<color name="deep_orange_50">#FBE9E7</color>
<color name="deep_orange_100">#FFCCBC</color>
<color name="deep_orange_200">#FFAB91</color>
<color name="deep_orange_300">#FF8A65</color>
<color name="deep_orange_400">#FF7043</color>
<color name="deep_orange_500">#FF5722</color>
<color name="deep_orange_600">#F4511E</color>
<color name="deep_orange_700">#E64A19</color>
<color name="deep_orange_800">#D84315</color>
<color name="deep_orange_900">#BF360C</color>
<color name="deep_orange_A100">#FF9E80</color>
<color name="deep_orange_A200">#FF6E40</color>
<color name="deep_orange_A400">#FF3D00</color>
<color name="deep_orange_A700">#DD2C00</color>
<!-- Deep Orange -->
<!-- Brown -->
<color name="brown_50">#EFEBE9</color>
<color name="brown_100">#D7CCC8</color>
<color name="brown_200">#BCAAA4</color>
<color name="brown_300">#A1887F</color>
<color name="brown_400">#8D6E63</color>
<color name="brown_500">#795548</color>
<color name="brown_600">#6D4C41</color>
<color name="brown_700">#5D4037</color>
<color name="brown_800">#4E342E</color>
<color name="brown_900">#3E2723</color>
<!-- Brown -->
<!-- Grey -->
<color name="grey_50">#FAFAFA</color>
<color name="grey_100">#F5F5F5</color>
<color name="grey_200">#EEEEEE</color>
<color name="grey_300">#E0E0E0</color>
<color name="grey_400">#BDBDBD</color>
<color name="grey_500">#9E9E9E</color>
<color name="grey_600">#757575</color>
<color name="grey_700">#616161</color>
<color name="grey_800">#424242</color>
<color name="grey_900">#212121</color>
<!-- Grey -->
<!-- Blue Grey -->
<color name="blue_grey_50">#ECEFF1</color>
<color name="blue_grey_100">#CFD8DC</color>
<color name="blue_grey_200">#B0BEC5</color>
<color name="blue_grey_300">#90A4AE</color>
<color name="blue_grey_400">#78909C</color>
<color name="blue_grey_500">#607D8B</color>
<color name="blue_grey_600">#546E7A</color>
<color name="blue_grey_700">#455A64</color>
<color name="blue_grey_800">#37474F</color>
<color name="blue_grey_900">#263238</color>
<!-- Blue Grey -->
<!-- Black -->
<color name="black">#000000</color>
<!-- Black -->
<!-- White -->
<color name="white">#FFFFFF</color>
<!-- White -->
<color name="blue_light">#6799FF</color>
</resources>
2. Model 생성
REST API 통신 시 받아올 데이터를 사용하기 쉽도록 응답받는 데이터에 맞추어 VO 를 구현한 Weather 관련 model class 입니당
JSON 을 사용할 경우 : @SerializedName("속성명") 으로 속성명 일치시켜주면 변수명 다르게도 가능
XML 을 사용할 경우 : @Element(name="속성명") XML은 @Element 어노테이션 사용
2-1. WeatherModel.kt
package com.eun.myweatherkotlin_mvvm_01.data.model
import com.google.gson.annotations.SerializedName
data class WeatherModel(
@SerializedName("name")
var name: String?, //도시이름
@SerializedName("weather")
var weather: List<WeatherWeatherModel>,
@SerializedName("main")
var main: WeatherMainModel,
@SerializedName("wind")
var wind: WeatherWindModel,
@SerializedName("sys")
var sys: WeatherSysModel,
@SerializedName("clouds")
var clouds: WeatherCloudsModel,
)
data class WeatherWeatherModel(
@SerializedName("main")
var main: String?, //날씨
@SerializedName("description")
var description: String?, //상세 날씨 설명
@SerializedName("icon")
var icon: String?, //날씨 이미지
)
data class WeatherMainModel(
@SerializedName("temp")
var temp: Double?, //현재온도
@SerializedName("humidity")
var humidity: Double?, //현재습도
)
data class WeatherWindModel(
@SerializedName("speed")
var speed: Double?, //바람
)
data class WeatherSysModel(
@SerializedName("country")
var country: String?, //나라
)
data class WeatherCloudsModel(
@SerializedName("all")
var all: Double?, //구름
)
2-2. ForecastModel.kt
package com.eun.myweatherkotlin_mvvm_01.data.model
import com.google.gson.annotations.SerializedName
data class ForecastModel(
@SerializedName("cod")
var cod: String?,
@SerializedName("cnt")
var cnt: Int?,
@SerializedName("list")
var list: List<ForecastListModel>?,
)
data class ForecastListModel(
@SerializedName("dt_txt")
var dt_txt: String?, //2021-08-26 15:00:00
@SerializedName("main")
var main: WeatherMainModel?,
@SerializedName("weather")
var weather: List<WeatherWeatherModel>?,
)
3. Network Service 생성
3-1. CookiesIntercepter.kt
package com.eun.myweatherkotlin_mvvm_01.data.network
import okhttp3.Interceptor
import okhttp3.Response
class CookiesIntercepter : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder().header("Content-Type", "application/json").build()
return chain.proceed(request)
}
}
3-2. WeatherAPIService.kt
Http Method(GET/POST/PUT/DELETE/HEAD) 와 자원의 정보를 정의할 인터페이스를 구현합니다.
필요에 따라 변수를 포함하여 정의합니다.
@GET(EndPoint URL/{path}) ...
ex) http://eunoia3jy.tistory.com/main/123 에서 main/123 이 EndPoint URL 이 됩니다.
package com.eun.myweatherkotlin_mvvm_01.data.network
import com.eun.myweatherkotlin_mvvm_01.data.model.ForecastModel
import com.eun.myweatherkotlin_mvvm_01.data.model.WeatherModel
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface WeatherAPIService {
@GET("data/2.5/{path}")
fun doGetJsonDataWeather(
@Path("path") path: String,
@Query("q") q: String,
@Query("appid") appid: String,
): Call<WeatherModel>
@GET("data/2.5/{path}")
fun doGetJsonDataForecast(
@Path("path") path: String,
@Query("id") id: String,
@Query("appid") appid: String,
): Call<ForecastModel>
}
3-3. WeatherAPIClient.kt
retrofit 객체를 초기화합니다.
baseUrl() 안의 url 은 꼭 / 로 끝나야 합니다. 아니면 예외 발생이 됩니다.
package com.eun.myweatherkotlin_mvvm_01.data.network
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object WeatherAPIClient {
fun getClient(url: String): Retrofit {
val okHttpClient: OkHttpClient = OkHttpClient.Builder().addInterceptor(CookiesIntercepter())
.addNetworkInterceptor(CookiesIntercepter()).build()
val retrofit: Retrofit = Retrofit.Builder().baseUrl(url)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
return retrofit
}
}
3-4. RemoteDataSource.kt
package com.eun.myweatherkotlin_mvvm_01.data.network
import com.eun.myweatherkotlin_mvvm_01.data.model.ForecastModel
import com.eun.myweatherkotlin_mvvm_01.data.model.WeatherModel
import org.json.JSONObject
import retrofit2.Response
interface RemoteDataSource {
fun getWeatherInfo(
jsonObject: JSONObject,
onResponse: (Response<WeatherModel>) -> Unit,
onFailure: (Throwable) -> Unit
)
fun getForecastInfo(
jsonObject: JSONObject,
onResponse: (Response<ForecastModel>) -> Unit,
onFailure: (Throwable) -> Unit
)
}
3-5. RemoteDataSourceImpl.kt
package com.eun.myweatherkotlin_mvvm_01.data.network
import com.eun.myweatherkotlin_mvvm_01.data.model.ForecastModel
import com.eun.myweatherkotlin_mvvm_01.data.model.WeatherModel
import org.json.JSONObject
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class RemoteDataSourceImpl : RemoteDataSource {
override fun getWeatherInfo(
jsonObject: JSONObject,
onResponse: (Response<WeatherModel>) -> Unit,
onFailure: (Throwable) -> Unit
) {
val APIService: WeatherAPIService = WeatherAPIClient.getClient(jsonObject.get("url").toString()).create(WeatherAPIService::class.java)
APIService.doGetJsonDataWeather(
jsonObject.get("path").toString(),
jsonObject.get("q").toString(),
jsonObject.get("appid").toString()
).enqueue(object :
Callback<WeatherModel> {
override fun onResponse(call: Call<WeatherModel>, response: Response<WeatherModel>) {
onResponse(response)
}
override fun onFailure(call: Call<WeatherModel>, t: Throwable) {
onFailure(t)
}
}
)
}
override fun getForecastInfo(
jsonObject: JSONObject,
onResponse: (Response<ForecastModel>) -> Unit,
onFailure: (Throwable) -> Unit
) {
val APIService: WeatherAPIService = WeatherAPIClient.getClient(jsonObject.get("url").toString()).create(WeatherAPIService::class.java)
APIService.doGetJsonDataForecast(
jsonObject.get("path").toString(),
jsonObject.get("id").toString(),
jsonObject.get("appid").toString()
).enqueue(object :
Callback<ForecastModel> {
override fun onResponse(call: Call<ForecastModel>, response: Response<ForecastModel>) {
onResponse(response)
}
override fun onFailure(call: Call<ForecastModel>, t: Throwable) {
onFailure(t)
}
}
)
}
}
4. Repository 생성
4-1. WeatherRepository.kt
WeatherRepository 클래스 에서는 ViewModel 에서 네트워크 통신을 요청할 때 수행할 함수를 만듭니다.
package com.eun.myweatherkotlin_mvvm_01.data.repository
import com.eun.myweatherkotlin_mvvm_01.data.model.ForecastModel
import com.eun.myweatherkotlin_mvvm_01.data.model.WeatherModel
import com.eun.myweatherkotlin_mvvm_01.data.network.RemoteDataSource
import com.eun.myweatherkotlin_mvvm_01.data.network.RemoteDataSourceImpl
import org.json.JSONObject
import retrofit2.Response
class WeatherRepository {
private val retrofitRemoteDataSource: RemoteDataSource = RemoteDataSourceImpl()
fun getWeatherInfo(
jsonObject: JSONObject,
onResponse: (Response<WeatherModel>) -> Unit,
onFailure: (Throwable) -> Unit,
) {
retrofitRemoteDataSource.getWeatherInfo(jsonObject, onResponse, onFailure)
}
fun getForecastInfo(
jsonObject: JSONObject,
onResponse: (Response<ForecastModel>) -> Unit,
onFailure: (Throwable) -> Unit,
) {
retrofitRemoteDataSource.getForecastInfo(jsonObject, onResponse, onFailure)
}
}
5. ViewModel 생성
ViewModel : View 를 위한 데이터를 가지고 있다.
액티비티의 생명주기에서 자유로울 순 없지만 ViewModel 은 View 와 ViewModel 의 분리로 액티비티가 destroy 되었다가 다시 create 되어도 종료되지 않고 가지고 있다.
5-1. WeatherViewModel.kt
package com.eun.myweatherkotlin_mvvm_01.viewmodel
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.eun.myweatherkotlin_mvvm_01.data.model.ForecastModel
import com.eun.myweatherkotlin_mvvm_01.data.model.WeatherModel
import com.eun.myweatherkotlin_mvvm_01.data.repository.WeatherRepository
import org.json.JSONObject
class WeatherViewModel : ViewModel() {
private val TAG: String = WeatherViewModel::class.java.simpleName
private val weatherRepository = WeatherRepository()
val isSuccWeather = MutableLiveData<Boolean>()
val isSuccForecast = MutableLiveData<Boolean>()
val responseWeather = MutableLiveData<WeatherModel>()
val responseForecast = MutableLiveData<ForecastModel>()
fun getWeatherInfoView(jsonObject: JSONObject) {
Log.d(TAG, "getWeatherInfoView() - jsonObject : " + jsonObject);
weatherRepository.getWeatherInfo(jsonObject = jsonObject,
onResponse = {
if (it.isSuccessful) {
Log.d(TAG, "getWeatherInfoView() - Succ : " + it.body());
isSuccWeather.value = true
responseWeather.value = it.body()
}
},
onFailure = {
it.printStackTrace()
Log.d(TAG, "getWeatherInfoView() - Fail : " + it.message);
}
)
}
fun getForecastInfoView(jsonObject: JSONObject) {
Log.d(TAG, "getForecastInfoView() - jsonObject : " + jsonObject);
weatherRepository.getForecastInfo(jsonObject = jsonObject,
onResponse = {
if (it.isSuccessful) {
isSuccForecast.value = true
Log.d(TAG, "getForecastInfoView() - Succ : " + it.body());
responseForecast.value = it.body()
}
},
onFailure = {
it.printStackTrace()
Log.d(TAG, "getForecastInfoView() - Fail : " + it.message);
}
)
}
}
6. View 생성
6-1. WeatherActivity.kt
WeatherActivity 의 화면을 ActivityWeatherBinding 을 통해 activity_weather.xml 의 view 를 표시합니다.
package com.eun.myweatherkotlin_mvvm_01.view
import android.content.Intent
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.annotation.RequiresApi
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.bumptech.glide.Glide
import com.eun.myweatherkotlin_mvvm_01.R
import com.eun.myweatherkotlin_mvvm_01.data.model.WeatherModel
import com.eun.myweatherkotlin_mvvm_01.databinding.ActivityWeatherBinding
import com.eun.myweatherkotlin_mvvm_01.util.doubleToStrFormat
import com.eun.myweatherkotlin_mvvm_01.viewmodel.WeatherViewModel
import org.json.JSONObject
@RequiresApi(Build.VERSION_CODES.M)
class WeatherActivity : AppCompatActivity() {
private val TAG: String = WeatherActivity::class.java.simpleName
private var viewModel: WeatherViewModel = WeatherViewModel()
private lateinit var binding: ActivityWeatherBinding //activity_weather.xml 을 바인딩
private lateinit var fabOpen: Animation
private lateinit var fabClose: Animation
private var isFabOpen: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_weather)
binding = ActivityWeatherBinding.inflate(layoutInflater)
setContentView(binding.root) //xml 전체를 감싸는 최상단 부모를 root 라는 property 로 제공
initView()
initWeatherInfoViewModel()
observeData()
}
/* View 설정 */
private fun initView() {
fabOpen = AnimationUtils.loadAnimation(this, R.anim.fab_open)
fabClose = AnimationUtils.loadAnimation(this, R.anim.fab_close)
binding.fabMain.setOnClickListener {
toggleFab(it)
}
binding.fabGoDetail.setOnClickListener { //DetailActivity 로 이동
val intent = Intent(this, DetailActivity::class.java)
startActivity(intent)
}
binding.fabGoShare.setOnClickListener { //공유하기
var valueText = "오늘의 날씨 : " + binding.tvTemp.text + " / " + binding.tvMain.text
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, valueText)
startActivity(Intent.createChooser(intent, getString(R.string.app_title)))
}
}
/* viewModel 설정 */
private fun initWeatherInfoViewModel() {
viewModel = ViewModelProvider(this).get(WeatherViewModel::class.java)
var jsonObject = JSONObject()
jsonObject.put("url", getString(R.string.weather_url))
jsonObject.put("path", "weather")
jsonObject.put("q", "Seoul")
jsonObject.put("appid", getString(R.string.weather_app_id))
viewModel.getWeatherInfoView(jsonObject)
}
/* viewModel observe 설정 */
private fun observeData() {
viewModel.isSuccWeather.observe(
this, Observer { it ->
if (it) {
viewModel.responseWeather.observe(
this, Observer {
setWeatherData(it)
}
)
} else {
//ERROR dialog 띄우기
var customDialog = CustomDialog(this)
customDialog.show(getString(R.string.app_title), "현재 날씨 조회 실패 ")
}
}
)
}
/* 통신하여 받아온 날씨 데이터를 통해 UI 업데이트 메소드 */
private fun setWeatherData(model: WeatherModel) {
binding.tvName.text = model.name
binding.tvCountry.text = model.sys.country
Glide.with(this).load(
resources.getDrawable(
resources.getIdentifier(
"icon_" + model.weather[0].icon,
"drawable",
packageName
)
)
)
.placeholder(R.drawable.icon_image)
.error(R.drawable.icon_image)
.into(binding.ivWeather)
binding.tvTemp.text = doubleToStrFormat(2, model.main.temp!! - 273.15) + " 'C"
binding.tvMain.text = model.weather[0].main
binding.tvDescription.text = model.weather[0].description
binding.tvWind.text = doubleToStrFormat(2, model.wind.speed!!) + " m/s"
binding.tvCloud.text = doubleToStrFormat(2, model.clouds.all!!) + " %"
binding.tvHumidity.text = doubleToStrFormat(2, model.main.humidity!!) + " %"
}
/* floatingActionButton 클릭 이벤트 시 호출 */
private fun toggleFab(view: View) {
if (isFabOpen) {
binding.fabMain.foreground = resources.getDrawable(
resources.getIdentifier(
"ic_add_24dp",
"drawable",
packageName
)
)
binding.fabGoShare.startAnimation(fabClose)
binding.fabGoDetail.startAnimation(fabClose)
isFabOpen = false
} else {
binding.fabMain.foreground = resources.getDrawable(
resources.getIdentifier(
"ic_close_24dp",
"drawable",
packageName
)
)
binding.fabGoShare.startAnimation(fabOpen)
binding.fabGoDetail.startAnimation(fabOpen)
isFabOpen = true
}
}
}
6-2. activity_weather.xml
WeatherActivity 에 대한 레이아웃입니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context=".view.WeatherActivity" >
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/amber_200"
android:elevation="10dp"
android:gravity="center"
android:text="현재 날씨"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/ll_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/ll_name"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="50dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginBottom="20dp"
android:weightSum="100"
android:orientation="horizontal"
android:background="@drawable/bg_custom_title_name"
android:gravity="center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_title" >
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="50"
android:layout_marginRight="2dp"
android:gravity="right|center_vertical"
android:text="Seoul"
android:textStyle="bold"
android:textColor="@color/white"
android:textSize="20dp" />
<TextView
android:id="@+id/tv_country"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="50"
android:layout_marginLeft="2dp"
android:gravity="left|center_vertical"
android:text="KR"
android:textStyle="bold"
android:textColor="@color/white"
android:textSize="20dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="30dp"
android:layout_marginHorizontal="20dp"
android:orientation="horizontal"
android:weightSum="100"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ll_name" >
<ImageView
android:id="@+id/iv_weather"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="30"
android:src="@drawable/icon_image"
android:background="@drawable/bg_solid" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_weight="70"
android:gravity="center_vertical"
android:orientation="vertical"
android:weightSum="100"
app:layout_constraintLeft_toRightOf="@+id/iv_weather"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<TextView
android:id="@+id/tv_temp"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="35"
android:gravity="center_vertical"
android:paddingLeft="25dp"
android:text="00 'C"
android:textColor="@color/blue_light"
android:textSize="25sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_main"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="35"
android:gravity="center_vertical"
android:paddingLeft="25dp"
android:text="Clear Sky"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_description"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="30"
android:gravity="center_vertical"
android:paddingLeft="25dp"
android:text="broken clouds"
android:textColor="@color/grey_500"
android:textSize="17sp" />
</LinearLayout>
</LinearLayout>
<View
android:id="@+id/view_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginVertical="30dp"
android:layout_marginHorizontal="20dp"
android:background="@color/grey_200"
app:layout_constraintBottom_toTopOf="@+id/ll_detail"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ll_main" />
<LinearLayout
android:id="@+id/ll_detail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="30dp"
android:layout_marginHorizontal="20dp"
android:orientation="horizontal"
android:weightSum="100"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/view_divider" >
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="33"
android:orientation="vertical"
android:weightSum="100" >
<ImageView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="45"
android:paddingTop="5dp"
android:src="@drawable/icon_wind" />
<TextView
android:id="@+id/nm_wind"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="15"
android:gravity="center"
android:text="바람 "
android:textColor="@color/grey_500"
android:textSize="13sp" />
<TextView
android:id="@+id/tv_wind"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="40"
android:gravity="center_horizontal"
android:text="4.6m/s"
android:textColor="@color/black"
android:textSize="17sp" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="33"
android:orientation="vertical"
android:weightSum="100" >
<ImageView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="45"
android:paddingTop="5dp"
android:src="@drawable/icon_cloud" />
<TextView
android:id="@+id/nm_cloud"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="15"
android:gravity="center"
android:text="구름 "
android:textColor="@color/grey_500"
android:textSize="13sp" />
<TextView
android:id="@+id/tv_cloud"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="40"
android:gravity="center_horizontal"
android:text="75%"
android:textColor="@color/black"
android:textSize="17sp" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="33"
android:orientation="vertical"
android:weightSum="100" >
<ImageView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="45"
android:gravity="center"
android:paddingTop="5dp"
android:src="@drawable/icon_humidity" />
<TextView
android:id="@+id/nm_humidity"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="15"
android:gravity="center"
android:text="습도 "
android:textColor="@color/grey_500"
android:textSize="13sp" />
<TextView
android:id="@+id/tv_humidity"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="40"
android:gravity="center_horizontal"
android:text="59%"
android:textColor="@color/black"
android:textSize="17sp" />
</LinearLayout>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp"
android:layout_marginLeft="20dp"
android:foreground="@drawable/ic_add_24dp"
android:backgroundTint="@color/dark_purple_200"
android:foregroundGravity="center"
app:borderWidth="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
></com.google.android.material.floatingactionbutton.FloatingActionButton>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_goDetail"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginBottom="40dp"
android:layout_marginLeft="20dp"
android:foreground="@drawable/ic_date_range_24dp"
android:backgroundTint="@color/dark_purple_100"
android:tint="@null"
android:foregroundGravity="center"
android:visibility="invisible"
app:borderWidth="0dp"
app:fabSize="mini"
app:layout_constraintStart_toEndOf="@id/fab_main"
app:layout_constraintBottom_toBottomOf="parent"
></com.google.android.material.floatingactionbutton.FloatingActionButton>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_goShare"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginBottom="40dp"
android:layout_marginLeft="10dp"
android:foreground="@drawable/ic_share_24dp"
android:backgroundTint="@color/dark_purple_100"
android:foregroundGravity="center"
android:visibility="invisible"
app:borderWidth="0dp"
app:fabSize="mini"
app:layout_constraintStart_toEndOf="@id/fab_goDetail"
app:layout_constraintBottom_toBottomOf="parent"
></com.google.android.material.floatingactionbutton.FloatingActionButton>
</androidx.constraintlayout.widget.ConstraintLayout>
6-3. DetailActivity.kt
DetailActivity 의 화면을 ActivityDetailBinding 을 통해 activity_detail.xml 의 view 를 표시합니다.
package com.eun.myweatherkotlin_mvvm_01.view
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.eun.myweatherkotlin_mvvm_01.R
import com.eun.myweatherkotlin_mvvm_01.databinding.ActivityDetailBinding
import com.eun.myweatherkotlin_mvvm_01.viewmodel.WeatherViewModel
import org.json.JSONObject
class DetailActivity : AppCompatActivity() {
private val TAG: String = DetailActivity::class.java.simpleName
private var viewModel: WeatherViewModel = WeatherViewModel()
private lateinit var binding: ActivityDetailBinding //activity_detail.xml 을 바인딩
private lateinit var listAdapter: ListAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_detail)
binding = ActivityDetailBinding.inflate(layoutInflater)
setContentView(binding.root) //xml 전체를 감싸는 최상단 부모를 root 라는 property 로 제공
initRecyclerview()
initForecastInfoViewModel()
observeData()
}
/*
* Recyclerview 설정
* Recyclerview adapter 와 LinearLayoutManager 를 만들고 연결
* GridLayoutManager : 그리드뷰로 표시
* */
private fun initRecyclerview() {
listAdapter = ListAdapter(this)
binding.recyclerviewList.run {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(this@DetailActivity)
adapter = listAdapter
}
//그리드뷰로 설정 (GridLayoutManager)
val gridLayoutManager = GridLayoutManager(this, 3)
binding.recyclerviewList.layoutManager = gridLayoutManager
}
/* viewModel 설정 */
private fun initForecastInfoViewModel() {
viewModel = ViewModelProvider(this).get(WeatherViewModel::class.java)
var jsonObject= JSONObject()
jsonObject.put("url", getString(R.string.weather_url))
jsonObject.put("path", "forecast")
jsonObject.put("id", getString(R.string.weather_seoul_id))
jsonObject.put("appid", this.getString(R.string.weather_app_id))
viewModel.getForecastInfoView(jsonObject)
}
/* viewModel observe 설정 */
private fun observeData() {
viewModel.isSuccForecast.observe(
this, Observer { it ->
if (it) {
viewModel.responseForecast.observe(
this, Observer {
listAdapter.setForecastItems(it.list!!)
}
)
} else {
//ERROR dialog 띄우기
var customDialog = CustomDialog(this)
customDialog.show(getString(R.string.app_title), "날씨 예보 조회 실패 ")
}
}
)
}
}
6-4. activity_detail.xml
DetailActivity 에 대한 레이아웃입니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.DetailActivity" >
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/amber_200"
android:elevation="10dp"
android:gravity="center"
android:text="날씨 예보"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/tv_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview_list"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@+id/tv_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:listitem="@layout/item_list"
android:foregroundGravity="center"
android:padding="10dp" ></androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>
6-5. ListAdapter.kt
ForecastListModel 의 리스트를 생상자로부터 전달받으며 RecycleView.Adapter 를 상속받고 RecyclerView.ListViewHolder 를 뷰홀더로 갖는 ListAdapter 클래스 를 만듭니다.
package com.eun.myweatherkotlin_mvvm_01.view
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.eun.myweatherkotlin_mvvm_01.R
import com.eun.myweatherkotlin_mvvm_01.data.model.ForecastListModel
import com.eun.myweatherkotlin_mvvm_01.util.convertForecastDate
import com.eun.myweatherkotlin_mvvm_01.util.doubleToStrFormat
class ListAdapter(context: Context): RecyclerView.Adapter<ListAdapter.ListViewHolder>() {
var mContext: Context = context
private var forecastItems: List<ForecastListModel> = listOf()
/*
* 이 어뎁터가 아이템을 얼마나 가지고 있는지 얻는 함수
* */
override fun getItemCount(): Int {
return forecastItems.size
}
/*
* 현재 아이템이 사용할 뷰홀더를 생성하여 반환하는 함수
* item_list 레이아웃을 사용하여 뷰를 생성하고 뷰홀더에 뷰를 전달하여 생성된 뷰홀더를 반환
* */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false)
val viewHolder = ListViewHolder(view)
return viewHolder
}
/*
* 현재 아이템의 포지션에 대한 데이터 모델을 리스트에서 얻고
* holder 객체를 ListViewHolder 로 형변환한 뒤 bind 메서드에 이 모델을 전달하여 데이터를 바인딩하도록 한다
* */
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
val forecastModel = forecastItems[position]
val listViewHolder = holder as ListViewHolder
listViewHolder.bind(forecastModel)
}
/* 데이터베이스가 변경될 때마다 호출 */
fun setForecastItems(forecastItems: List<ForecastListModel>) {
this.forecastItems = forecastItems
notifyDataSetChanged()
}
/*
* 뷰홀더는 리스트를 스크롤하는 동안 뷰를 생성하고 다시 뷰의 구성요소를 찾는 행위를 반복하면서 생기는
* 성능저하를 방지하기 위해 미리 저장 해 놓고 빠르게 접근하기 위하여 사용하는 객체
* */
inner class ListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tv_detail_date = itemView.findViewById<TextView>(R.id.tv_detail_date)
private val iv_detail_img = itemView.findViewById<ImageView>(R.id.iv_detail_img)
private val tv_detail_main = itemView.findViewById<TextView>(R.id.tv_detail_main)
private val tv_detail_temp = itemView.findViewById<TextView>(R.id.tv_detail_temp)
fun bind(forecastModel: ForecastListModel) {
tv_detail_date.text = forecastModel?.dt_txt!!.convertForecastDate()
iv_detail_img.setImageResource(mContext.resources.getIdentifier("icon_"+forecastModel?.weather?.get(0)?.icon, "drawable", mContext.packageName))
tv_detail_main.text = forecastModel?.weather?.get(0)?.main
tv_detail_temp.text = doubleToStrFormat(2,
forecastModel?.main?.temp!!.minus(273.15)
)+" 'C"
}
}
}
6-6. item_list.xml
RecyclerView 에 들어가는 아이템 xml 입니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
app:cardCornerRadius="20dp" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="140dp"
android:weightSum="100"
android:padding="10dp"
android:orientation="vertical" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="20"
android:weightSum="100"
android:orientation="horizontal" >
<TextView
android:id="@+id/tv_detail_date"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="100"
android:text="08/27 13 시 "
android:textStyle="bold"
android:textColor="@color/grey_600"
android:textSize="12sp"
android:gravity="left|center_vertical" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="40"
android:weightSum="100"
android:orientation="horizontal" >
<ImageView
android:id="@+id/iv_detail_img"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="100"
android:src="@drawable/icon_image" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="20"
android:weightSum="100"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_detail_main"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="100"
android:text="RAIN"
android:textColor="@color/black"
android:textSize="15sp"
android:gravity="center" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="20"
android:weightSum="100"
android:orientation="horizontal" >
<TextView
android:id="@+id/tv_detail_temp"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="100"
android:text="22 'C "
android:textStyle="bold"
android:textColor="@color/blue_light"
android:textSize="15sp"
android:gravity="center" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
6-7. CustomDialog.kt
오류 발생 시 표시하는 Dialog 의 화면을 LayoutDialogBinding 을 통해 layout_dialog.xml 의 view 를 표시합니다.
package com.eun.myweatherkotlin_mvvm_01.view
import android.app.Dialog
import android.content.Context
import com.eun.myweatherkotlin_mvvm_01.databinding.LayoutDialogBinding
class CustomDialog(context: Context) {
lateinit var binding: LayoutDialogBinding
private val dialog = Dialog(context)
fun show(title: String, content: String) {
binding = LayoutDialogBinding.inflate(dialog.layoutInflater)
dialog.setContentView(binding.root)
// dialog.setContentView(R.layout.layout_dialog)
if(title != "") {
binding.dialogTitle.text = title
}
if(content != "") {
binding.dialogContent.text = content
}
initView()
dialog.show()
}
fun initView() {
binding.btnConfirm.setOnClickListener {
android.os.Process.killProcess(android.os.Process.myPid()); //앱 종료
}
binding.btnCancel.setOnClickListener {
dialog.dismiss()
}
}
}
6-8. layout_dialog.xml
CustomDialog 에 대한 레이아웃입니다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<LinearLayout
android:id="@+id/ll_topbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintRight_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="330dp"
android:layout_height="250dp"
android:orientation="vertical"
android:weightSum="100">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="20"
android:background="@color/amber_200">
<TextView
android:id="@+id/dialog_title"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center"
android:text="@string/app_name"
android:textColor="@color/black"
android:textSize="16dp" />
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="55"
android:background="@color/white">
<TextView
android:id="@+id/dialog_content"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@color/white"
android:gravity="center"
android:lineSpacingExtra="5dp"
android:padding="20dp"
android:text="다이얼로그 알림"
android:textColor="@color/black"
android:textSize="15dp" />
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="25"
android:background="@color/white"
android:gravity="center"
android:orientation="horizontal"
android:weightSum="100">
<LinearLayout
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_marginRight="7dp"
android:layout_weight="40"
android:gravity="top"
android:orientation="vertical"
android:weightSum="100">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_confirm"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="55"
android:background="@drawable/custom_dialog_button"
android:gravity="center"
android:text="확 인"
android:textColor="@color/white"
android:textSize="14dp" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_marginLeft="7dp"
android:layout_weight="40"
android:gravity="top"
android:orientation="vertical"
android:weightSum="100">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_cancel"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="55"
android:background="@drawable/custom_dialog_button"
android:gravity="center"
android:text="취 소"
android:textColor="@color/white"
android:textSize="14dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
감사합니당!
※ 잘못된 정보가 있을 경우 댓글로 알려주시면 수정하겠습니다 🙂
'📱 안드로이드 Android ~ Kotlin' 카테고리의 다른 글
[안드로이드/Android] 클린아키텍처 Clean Architecture (0) | 2021.09.09 |
---|---|
[Android/Firebase] Firebase 프로젝트 생성 및 앱 추가 (0) | 2021.09.05 |
[Android/Kotiln] 데이터 바인딩 dataBinding 사용하기 (0) | 2021.09.02 |
[Android/Kotiln] MVVM 적용한 ToDo 앱 만들기 (LiveData) (0) | 2021.08.13 |
안드로이드 아키텍쳐 컴포넌트 AAC (Android Architecture Components) (0) | 2021.08.06 |