📱 안드로이드 Android ~ Kotlin

[Android/Kotiln] MVVM 적용한 날씨 앱 만들기 (viewBinding, retrofit 사용)

핑크빛연어 2021. 8. 30. 23:15

 

viewBinding


findViewById를 쓰지 않고, XML의 view component 에 접근하는 object를 반환받아 view에 접근하는 방식

https://developer.android.com/topic/libraries/view-binding?hl=ko 

 

뷰 결합  |  Android 개발자  |  Android Developers

뷰 결합 뷰 결합 기능을 사용하면 뷰와 상호작용하는 코드를 쉽게 작성할 수 있습니다. 모듈에서 사용 설정된 뷰 결합은 모듈에 있는 각 XML 레이아웃 파일의 결합 클래스를 생성합니다. 바인딩

developer.android.com

 

 

🚨 장점

 

1. findViewById()를 사용하지 않아도 된다. 자동으로 xml에서 만든 View들을 만들어준다.
2. Null 안정성 - 개발자가 실수로 유효하지 않은 id를 사용할 때 발생하는 null 오류를 방지할 수 있다
3. Type 안정성 - TextView의 타입을 ImageView라고 잘못 적어서 발생하는 cast exception 을 방지할 수 있다.
4. findViewById 를 사용했을 때 보다 속도가 상대적으로 빠르다.

 

 

🚨 사용 방법

 

build.gradle(:app)

viewBindingenabledtrue 로 설정합니다. 안드로이드 스튜디오의 버전에 따라 표시방법이 다릅니다.

// 안드로이드 스튜디오 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>

 

 

🚨 예제 코드

 

안드로이드에서 서버와 통신하기 위한 retrofitviewBinding 을 이용하여 MVVM 패턴을 적용한 예제 코드를 openweathermap 의 api 를 통해 날씨 정보를 표시해주는 앱으로 만들어 보았습니다. 🌤

https://openweathermap.org/api

 

Weather API - OpenWeatherMap

Please, sign up to use our fast and easy-to-work weather APIs for free. In case your requirements go beyond our freemium account conditions, you may check the entire list of our subscription plans. You can read the How to Start guide and enjoy using our po

openweathermap.org

 

결과 화면을 먼저 보여드릴게요!

 

결과 화면

 

WeatherActivity 의 화면입니다.

하단 FloatingActionButton 에 open/close 효과를 추가하여 상세페이지로 이동하는 버튼, 현재 날씨를 공유하는 버튼을 만들었습니다.

WeatherActivity 의 화면

 

DetailActivity 화면은 recyclerview 를 Grid 형태로 만들어서 4일간의 일기예보를 cardView 형태로 표시해주었습니다.

openweathermap 에서 제공하는 아이콘이 있지만 저는 drawable 에 아이콘들을 추가하여 사용하였습니다.

오류 발생 시 Dialog 를 표시하여 확인 버튼을 클릭 시 앱을 종료하도록 하였습니다.

DetailActivity 화면 / CustomDialog 화면

 

 

작성한 파일 목록 입니다.


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>

 

감사합니당!

※ 잘못된 정보가 있을 경우 댓글로 알려주시면 수정하겠습니다 🙂

 

 

728x90
반응형