├── .github └── ISSUE_TEMPLATE │ └── ---------.md ├── .gitignore ├── 001. Jetpack Compose 동작 원리 분석하기 ├── 001_Jetpack_part1.md ├── 001_Jetpack_part2.md ├── 001_Jetpack_part3.md ├── heesungbae.md ├── leekijung.md ├── namhoonkim.md ├── siyoungsong_pt1.md └── siyoungsong_pt2.md ├── 002. Kotlin Coroutine ├── heesungbae.md ├── kijunglee_pt1.md ├── meeting.md ├── namhoonkim.md ├── seheejeong-2.md ├── seheejeong.md ├── siyoungsong.md └── siyoungsong_concurrency.md ├── 003. View Rendering And Optimization ├── 003_Android_UI_Rendering_Optimization_part1.md ├── 003_Android_UI_Rendering_Optimization_part2.md ├── 003_Android_UI_Rendering_Optimization_part3.md ├── heesungbae.md ├── leekijung.md ├── namhoon.md ├── sehee-2.md ├── sehee.md ├── siyoung_Profiling.md ├── siyoungsong.md └── soyeongChoi.md ├── 004. Image Loading and Caching Library ├── 004_Image Loading and Caching Library_Part1.md ├── 004_Image Loading and Caching Library_Part2.md ├── 004_Image Loading and Caching Library_Part3.md ├── heesungbae.md ├── namhoon1.md ├── namhoon2.md ├── sehee1.md ├── sehee2.md ├── siyoung.md ├── siyoung_appendix.md └── toc.md ├── 005. Kotlin Symbol Processing API ├── All about KSP - Part1 Annotation과 KAPT.md ├── All about KSP - Part2 KSP 살펴보기.md ├── KSP 3차합본 by Charlezz.md ├── charlezz.md ├── leekijung.md ├── leekijung2.md ├── namhoon_1.md ├── namhoon_2.md ├── sehee.md ├── siyoung.md ├── siyoung2.md ├── toc.md ├── ukhyun-part1.md └── ukhyun-part2.md ├── 006. Android System UI ├── img │ └── auSbY.png ├── leekijung_01.md ├── leekijung_02.md ├── leekijung_03.md ├── namhoon_01.md ├── sehee.md ├── sehee0.md ├── siyoung_ime.md ├── siyoung_temp.md ├── toc.md ├── uhkwak-1.md ├── uhkwak-2.md └── 전체화면 설정하기(immersive mode).md ├── 007. MVVM V.S. MVI ├── 007_MVVM V.S MVI Part 1.md ├── 007_MVVM V.S MVI Part 2.md ├── 007_MVVM V.S MVI Part 3.md ├── 007_MVVM V.S MVI Part 4.md ├── 007_MVVM V.S MVI Part 5.md ├── ART │ └── mvi_01.png ├── ArchPatternSample │ ├── .gitignore │ ├── build.gradle.kts │ ├── buildSrc │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── main │ │ │ └── kotlin │ │ │ └── Dependencies.kt │ ├── data │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── consumer-rules.pro │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── androidTest │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── charlezz │ │ │ │ └── data │ │ │ │ └── ExampleInstrumentedTest.kt │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── charlezz │ │ │ │ └── data │ │ │ │ └── flickr │ │ │ │ ├── FlickrPagingSource.kt │ │ │ │ ├── FlickrPhoto.kt │ │ │ │ ├── FlickrPhotoMapper.kt │ │ │ │ ├── FlickrPhotos.kt │ │ │ │ ├── FlickrPhotosRepository.kt │ │ │ │ ├── FlickrResult.kt │ │ │ │ ├── FlickrRetrofitModule.kt │ │ │ │ └── FlickrService.kt │ │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── charlezz │ │ │ └── data │ │ │ ├── FlickrPagingSourceTest.kt │ │ │ ├── FlickrServiceTest.kt │ │ │ └── fake │ │ │ └── FakeFlickrService.kt │ ├── domain │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── consumer-rules.pro │ │ ├── proguard-rules.pro │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── com │ │ │ └── charlezz │ │ │ └── domain │ │ │ ├── Photo.kt │ │ │ ├── repository │ │ │ └── PhotosRepository.kt │ │ │ └── usecase │ │ │ └── SearchUseCase.kt │ ├── gradle.properties │ ├── gradle.properties.kts │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── mvi │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── androidTest │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── charlezz │ │ │ │ └── mvi │ │ │ │ └── ExampleInstrumentedTest.kt │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── charlezz │ │ │ │ │ └── mvi │ │ │ │ │ └── ui │ │ │ │ │ ├── PhotoActivity.kt │ │ │ │ │ └── PhotoViewModel.kt │ │ │ └── res │ │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ │ ├── drawable │ │ │ │ └── ic_launcher_background.xml │ │ │ │ ├── layout │ │ │ │ └── activity_main.xml │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ │ └── values │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── themes.xml │ │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── charlezz │ │ │ └── mvi │ │ │ └── ExampleUnitTest.kt │ ├── mvvm │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── androidTest │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── charlezz │ │ │ │ └── mvvm │ │ │ │ └── ExampleInstrumentedTest.kt │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── charlezz │ │ │ │ │ └── mvvm │ │ │ │ │ ├── App.kt │ │ │ │ │ ├── di │ │ │ │ │ └── AppModules.kt │ │ │ │ │ ├── ui │ │ │ │ │ ├── BindableViewHolder.kt │ │ │ │ │ ├── PhotoActivity.kt │ │ │ │ │ ├── PhotoAdapter.kt │ │ │ │ │ ├── PhotoUiModel.kt │ │ │ │ │ └── PhotoViewModel.kt │ │ │ │ │ └── util │ │ │ │ │ └── ImageViewBindingAdapter.kt │ │ │ └── res │ │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ │ ├── drawable │ │ │ │ └── ic_launcher_background.xml │ │ │ │ ├── layout │ │ │ │ ├── activity_photo.xml │ │ │ │ └── view_photo.xml │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ │ └── values │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── themes.xml │ │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── charlezz │ │ │ └── mvvm │ │ │ └── ExampleUnitTest.kt │ └── settings.gradle.kts ├── MVVM, MVI.md ├── MVVMPattern.png ├── img │ └── mvvm.png ├── jr_choe_01.md ├── jr_choe_02.md ├── jr_choe_03.md ├── leekijung_01.md ├── leekijung_02.md ├── mvi.md ├── mvvm-uhkwak(1).md ├── mvvm-uhkwak(2).md ├── siyoung.md └── toc.md ├── 008. KeyStore System and Security └── toc.md └── readme.md /.github/ISSUE_TEMPLATE/---------.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 주제 등록 템플릿 3 | about: 주제 등록시 활용할 템플릿입니다. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### 주제 선정 배경 11 | 예시) 12 | NavigationBarColor 쪽을 개발하다가 Api 27을 기준으로 분리가 됨을 알았다. 상단의 Status Bar와 함께 SystemUI라고 불리는 것들이 있는데, Application Layer가 아니다보니 친해질 기회가 적었다. 13 | 14 | 이 기회에 Android의 System UI가 뭔지 알아보고 싶다. 15 | 16 | ### 참고 자료 17 | 예시) 18 | https://source.android.com/devices/automotive/hmi/system_ui 19 | https://developer.android.com/training/system-ui 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/androidstudio 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio 4 | 5 | ### AndroidStudio ### 6 | # Covers files to be ignored for android development using Android Studio. 7 | 8 | # Built application files 9 | *.apk 10 | *.ap_ 11 | *.aab 12 | 13 | # Files for the ART/Dalvik VM 14 | *.dex 15 | 16 | # Java class files 17 | *.class 18 | 19 | # Generated files 20 | bin/ 21 | gen/ 22 | out/ 23 | 24 | # Gradle files 25 | .gradle 26 | .gradle/ 27 | build/ 28 | 29 | # Signing files 30 | .signing/ 31 | 32 | # Local configuration file (sdk path, etc) 33 | local.properties 34 | 35 | # Proguard folder generated by Eclipse 36 | proguard/ 37 | 38 | # Log Files 39 | *.log 40 | 41 | # Android Studio 42 | /*/build/ 43 | /*/local.properties 44 | /*/out 45 | /*/*/build 46 | /*/*/production 47 | captures/ 48 | .navigation/ 49 | *.ipr 50 | *~ 51 | *.swp 52 | 53 | # Keystore files 54 | *.jks 55 | *.keystore 56 | 57 | # Google Services (e.g. APIs or Firebase) 58 | # google-services.json 59 | 60 | # Android Patch 61 | gen-external-apklibs 62 | 63 | # External native build folder generated in Android Studio 2.2 and later 64 | .externalNativeBuild 65 | 66 | # NDK 67 | obj/ 68 | 69 | # IntelliJ IDEA 70 | *.iml 71 | *.iws 72 | /out/ 73 | 74 | # User-specific configurations 75 | .idea/caches/ 76 | .idea/libraries/ 77 | .idea/shelf/ 78 | .idea/workspace.xml 79 | .idea/tasks.xml 80 | .idea/.name 81 | .idea/compiler.xml 82 | .idea/copyright/profiles_settings.xml 83 | .idea/encodings.xml 84 | .idea/misc.xml 85 | .idea/modules.xml 86 | .idea/scopes/scope_settings.xml 87 | .idea/dictionaries 88 | .idea/vcs.xml 89 | .idea/jsLibraryMappings.xml 90 | .idea/datasources.xml 91 | .idea/dataSources.ids 92 | .idea/sqlDataSources.xml 93 | .idea/dynamic.xml 94 | .idea/uiDesigner.xml 95 | .idea/assetWizardSettings.xml 96 | .idea/gradle.xml 97 | .idea/jarRepositories.xml 98 | .idea/navEditor.xml 99 | 100 | # OS-specific files 101 | .DS_Store 102 | .DS_Store? 103 | ._* 104 | .Spotlight-V100 105 | .Trashes 106 | ehthumbs.db 107 | Thumbs.db 108 | 109 | # Legacy Eclipse project files 110 | .classpath 111 | .project 112 | .cproject 113 | .settings/ 114 | 115 | # Mobile Tools for Java (J2ME) 116 | .mtj.tmp/ 117 | 118 | # Package Files # 119 | *.war 120 | *.ear 121 | 122 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 123 | hs_err_pid* 124 | 125 | ## Plugin-specific files: 126 | 127 | # mpeltonen/sbt-idea plugin 128 | .idea_modules/ 129 | 130 | # JIRA plugin 131 | atlassian-ide-plugin.xml 132 | 133 | # Mongo Explorer plugin 134 | .idea/mongoSettings.xml 135 | 136 | # Crashlytics plugin (for Android Studio and IntelliJ) 137 | com_crashlytics_export_strings.xml 138 | crashlytics.properties 139 | crashlytics-build.properties 140 | fabric.properties 141 | 142 | ### AndroidStudio Patch ### 143 | 144 | !/gradle/wrapper/gradle-wrapper.jar 145 | 146 | # End of https://www.toptal.com/developers/gitignore/api/androidstudio -------------------------------------------------------------------------------- /001. Jetpack Compose 동작 원리 분석하기/001_Jetpack_part3.md: -------------------------------------------------------------------------------- 1 | # Jetpack Compose Part 3 - Retrospect 2 | 3 | **Author in [AndroidDeepDive Study](https://github.com/AndroidDeepDive/Study)** 4 | - 김남훈 @Naver 5 | - 배희성 @RocketPunch 6 | - 송시영 @SmartStudy 7 | - 이기정 @BankSalad 8 | 9 | ### Jetpack Compose의 Coupling과 Cohesion 10 | 11 | 비단 Android 애플리케이션뿐만 아니라, 소프트웨어를 잘 개발하기 위해 지켜야할 원칙 중 하나는 **관심사의 분리(Separation of concerns)** 라는 개념이다. 12 | 13 | 흔히 복잡성을 극복하기 위해 여러가지 디자인 패턴을 적용하여 View와 Controller를 분리하기 위해 애쓰는 사람들이 많은 것도 결국 프로덕트의 복잡성을 (완전히 없앨 수는 있을까?) 최소화하기 위해서이다. 14 | 15 | Compose 또한 이 복잡성의 극복을 위해 나온 도구라고 봐도 무방할 것이다. 16 | 17 | 따라서 Compose를 적용하면서 계속 머릿속에 캐시해두어야할 개념은 **Coupling** 과 **Cohesion** 이다. 18 | 19 | 흔히들 커플링이 심하다라고 표현하긴 하는데, 굳이 한글로 번역하자면 각각, 어떠한 컴포넌트들간의 **결속** 과 분리된 각 모듈 내부의 **응집** 을 뜻한다고 볼 수 있다. 20 | 21 | ![](https://drive.google.com/uc?export=view&id=18L_Hb3HB5wIIBS-lD1pu3uuA2FsCxvoJ) 22 | 23 | > **출처** [How Cohesion and Coupling Correlate](https://blog.ttulka.com/how-cohesion-and-coupling-correlate) 24 | 25 | 위의 세 그림을 아래 개념을 뜻한다. 26 | 27 | - A : Low cohesion, tight coupling 28 | - B : High cohesion, tight coupling 29 | - C : High cohesion, loose coupling 30 | 31 | 결국 Compose를 잘 쓴다는 것은 UI/UX 입장에서 뷰를 잘 분리하여 재사용을 통해 우리가 개발하려는 프로덕트가 C의 구조가 되게끔 작성하는 것이다. 32 | 33 | ### Compose 사용시 주의사항 34 | 35 | - 최대한 재사용성이 높게 설계되어야 한다. 36 | - 뷰 모델과 비지니스 모델 분리가 필요하다. 37 | - 지속적으로 재사용하는데 ViewModel과 같은 비지니스 모델이 수행되면 무거워지기 때문 38 | - 매개변수나 람다를 이용하자. 39 | - 호출 순서가 보장되지 않으므로, 각 함수는 독립적으로 설계해야 한다. 40 | 41 | ### Compose의 State 42 | 43 | **remember** 44 | - 예약어를 통해 메모리에 단일 객체를 저장 가능하다. 45 | - 상위 컴포넌트가 재구성되기 전까지 상태를 가지고 있다. 46 | 47 | **Stateless** 48 | - Satateless Composable은 말그대로 상태값을 가지고 있지 않다. 49 | - 외부에서 매개변수나 람다를 통해 상태를 받아 사용한다. 50 | 51 | **Stateful** 52 | - 상태 값을 가지고 있는 Composable 53 | - `State` 객체를 통해 관찰가능한 값을 가지고 있으므로 사용이 권장된다. 54 | - Runtime시 통합된다. 55 | - 이전 상태를 복원하기 위해서는 `rememberSaveable` 키워드를 사용한다. 56 | - `Bundle`에 데이터가 자동으로 저장된다. 57 | 58 | #### Compose Samples 59 | 60 | 코드랩 뿐만 아니라 다양한 Compose 샘플들을 구글이 모아두었다. 61 | 62 | 아래 링크를 참조하자. 63 | 64 | > [Github#Android Compose Samples](https://github.com/android/compose-samples) 65 | 66 | 현재 작업하고 있는 뷰와 제일 유사한 것들을 통해 Compose를 이해하는 것도 하나의 방법이 될 수 있다. 67 | 68 | ### 마무리하며 69 | 70 | - 뷰를 제작함에 있어서 선언형 프로그래밍은 좋은 효율을 낼 수 있을 것 같다 71 | - Flutter나 SwiftUI를 사용해보면 뷰 제작 속도가 얼마나 다른지 알 수 있다. (개개인의 차이가 있을 수 있음) 72 | 73 | - 요즘 제일 많이 사용하는 MVVM 아키텍처도 적용 가능하지만 이게 적합한지는 잘 모르겠다. 74 | - MVI 같은 아키텍처가 오히려 더 잘 맞을 수 있다고 생각한다. 75 | 76 | - 안드로이드에서 전혀 생각지도 못한 선언형 프로그래밍 방식을 도입할 수 있던 것이 신선하다고 생각한다. 77 | - 선언형 프로그래밍도 결국 유행하지않을까? 78 | - 기존에 React나 Flutter를 통해 선언형 프로그래밍을 쉽게 접했던 개발자들은 더 쉽게 안드로이드 개발을 도전해보지 않을까 싶다. 79 | - 오히려 반대로 기존에 XML로 레이아웃 코드를 짜던 개발자들은 생소한 방식이라 적응하는데 시간이 꽤나 걸릴 것이다. 80 | 81 | - 다만, 아직도 alpha버전이라 바뀔 수 있는 것이 너무나도 많은 상황이다. 82 | - 앞으로 좋은 예시들이 나오고, 좀 더 기존 안드로이드 개발자들이 익숙해 할만한 컴포넌트로 제공이 되면, 좀 더 많은 개발자들이 선언형 프로그래밍으로 넘어갈 수 있지 않을까 싶다. 83 | - 당장 기존에 있던 코드와 공존하면서 쓰기에는 여간 불편할것 같다. 84 | - Flutter의 경우 선언형 프로그래밍 방식으로 렌더링을 지원하기 때문에 처음부터 개발을 선언형으로 하게되니, 굳이 이럴바에는 Flutter로 서비스를 따로 구현하는 게 낫지 않을까라는 생각이 들었다. 85 | 86 | 87 | ## References 88 | 89 | ### Members of Study 90 | - https://namhoon.kim/2021/03/14/android_deep_dive/001/ 91 | - https://namhoon.kim/2021/03/21/android_deep_dive/002/ 92 | - https://sysys.medium.com/jetpack-compose-%EC%B2%B4%ED%97%98%EA%B8%B0-8f90aff89f47 93 | - https://soda1127.github.io/introduce-jetpack-compose/ 94 | 95 | ### Official 96 | - [Android Developers#Jetpack Compose](https://developer.android.com/jetpack/compose/) 97 | - [Android Developers#Compose 이해](https://developer.android.com/jetpack/compose/mental-model?hl=ko) 98 | - [Android Developers#Compose Cource](https://developer.android.com/courses/pathways/compose) 99 | - [Android#Compose Samples](https://github.com/android/compose-samples) 100 | 101 | ### Etc 102 | - [Understanding Jetpack Compose - part 1 of 2](https://medium.com/androiddevelopers/understanding-jetpack-compose-part-1-of-2-ca316fe39050) 103 | - [Understanding Jetpack Compose - part 2 of 2](https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd) 104 | - [foso.github.io](https://foso.github.io/Jetpack-Compose-Playground/) 105 | -------------------------------------------------------------------------------- /001. Jetpack Compose 동작 원리 분석하기/heesungbae.md: -------------------------------------------------------------------------------- 1 | # Compose 동작원리 정리 2 | 3 | ## Compose 란? 4 | 5 | - 선언형 UI 도구 키트이다 6 | - 기존에 뷰를 업데이트 할 때 수동적으로 조작(여러 위치에서 데이터 변경을 통한 렌더링)하면 오류가 발생할 가능성이 높습니다 7 | - Compose는 필요한 지점에 대해서만 뷰를 다시 그려주는 방식으로 동작합니다 8 | - @Composable 어노테이션을 사용한 함수를 사용합니다 9 | 10 | ```kotlin 11 | @Composable 12 | fun Greeting(name: String) { 13 | Text("Hello $name") 14 | } 15 | ``` 16 | 17 | - UI를 구성하는 것이 아닌 화면 상태를 설명하는 것이므로 아무것도 반환할 필요가 없습니다ㅣ 18 | 19 | ## Compose 사용 주의사항 20 | 21 | - 최대한 재사용성이 높게 설계되어야합니다 22 | - 뷰 모델과 비지니스 모델 분리가 필요합니다 → 지속적으로 재사용하는데 ViewModel과 같은 비지니스 모델이 수행되면 무거워집니다 → 매개변수나 람다를 이용합니다 23 | - 각 함수는 동립적으로 설계가 되어아합니다 호출 순서가 보장되지 않아 동시에 실행가능하기 때문입니다 24 | - Composable 매개변수가 변경되면 다시 그립니다 25 | 26 | ## Compose의 상태 27 | 28 | - remember 예약어를 통해 메모리에 단일 객체를 저장 가능합니다 → 해당 상위 컴포넌트가 재구성되기 전까지 상태를 가지고 있습니다 29 | - Stateless 컴포저블은 내부에 상태 값을 가지고 있지 않는 컴포저블 입니다 → 외부를 통해서 매개변수나 람다를 사용하여 상태를 받아 사용합니다 30 | - Stateful 컴포저블은 내부에 상태 값을 가지고 있는 컴포저블 입니다 → State 관찰가능한 상태를 내장되어 있어 사용을 권장합니다 → 런타임시 통합됩니다 31 | - 이전 상태를 복원하기 위해서는 rememberSaveable 예약어를 사용합니다 32 | - Bundle에 데이터가 자동으로 저장됩니다 33 | 34 | ## Compose 정리 35 | 36 | - 뷰를 제작함에 있어서 선언형 프로그래밍은 좋은 효율을 낼 수 있을 것 같다 → Flutter나 SwiftUI를 사용해보면 뷰 제작 속도가 얼마나 다른지 알 수 있습니다(개개인의 차이가 있을 수 있음) 37 | - 요즘 제일 많이 사용하는 MVVM 아키텍처도 적용 가능하지만 이게 적합한지는 잘 모르겠습니다 → MVI 같은 아키텍처가 오히려 더 잘 맞을 수 있다고 생각합니다 38 | -------------------------------------------------------------------------------- /001. Jetpack Compose 동작 원리 분석하기/siyoungsong_pt2.md: -------------------------------------------------------------------------------- 1 | 2 | ## Jetpack Compose 는 어떻게 동작할까? 3 | 4 | * *Jetpack Compose 는 안드로이드스튜디오 카나리 버전에서 사용이 가능합니다. (Android Studio Canary 9 기준으로 작성되었습니다)* 5 | 6 | 7 | 8 | ## Activity 에서 UI 를 구성하는 방법 9 | 10 | ### setContentView 11 | 12 | * 일반적으로 Activity 에서 UI 를 구성할 때 사용합니다. 13 | > Set the activity content from a layout resource. The resource will be inflated, adding all top-level views to the activity. 14 | > Set the Activity’s content view to the given layout and return the associated binding. The given layout resource must not be a merge layout. 15 | 16 | 레이아웃 리소스로 부터 Activity 콘텐츠 설정할 수 있습니다. 리소스는 확장되며, 모든 최상위 레벨 뷰는 Activity 에 추가됩니다. 17 | 18 | 레이아웃과 레이아웃에 바인딩된 결과를 통해서 Activity 콘텐츠를 설정할 수 있습니다. 레이아웃 리소스는 merge 레이아웃이 아니여야 합니다. 19 | 20 | 21 | 22 | ### setContent 23 | 24 | * Jetpack Compose 를 통해서 UI 를 구성할 때 사용합니다. 25 | > A android.view.View that can host Jetpack Compose UI content. Use setContent to supply the content composable function for the view. 26 | > Set the Jetpack Compose UI content for this view. Initial composition will occur when the view becomes attached to a window or when createComposition is called, whichever comes first. 27 | 28 | android.view.View 는 Jetpack Compose UI 를 사용할 수 있게 하며, setContent 를 사용하여 composable function 를 제공할 수 있습니다. Jetpack Compose 는 뷰가 윈도우에 attached 되는 시점 혹은 createComposition 이 호출 시점 중 빠른 시점에 구성 초기화가 됩니다. 29 | 30 | 초기화 되는 코드를 살펴보면 아래와 같이 ComponentActivity.setContent 를 통해 Jetpack Compose 를 구성하고 초기화할 수 있습니다. 31 | 32 | public fun ComponentActivity.setContent( 33 | parent: CompositionContext? = null, 34 | content: @Composable () -> Unit 35 | ) { 36 | val existingComposeView = window.decorView 37 | .findViewById(android.R.id.content) 38 | .getChildAt(0) as? ComposeView 39 | 40 | if (existingComposeView != null) with(existingComposeView) { 41 | setParentCompositionContext(parent) 42 | setContent(content) 43 | } else ComposeView(this).apply { 44 | // Set content and parent **before** setContentView 45 | // to have ComposeView create the composition on attach 46 | setParentCompositionContext(parent) 47 | setContent(content) 48 | // Set the view tree owners before setting the content view so that the inflation process 49 | // and attach listeners will see them already present 50 | setOwners() 51 | setContentView(this, DefaultActivityContentLayoutParams) 52 | } 53 | } 54 | 55 | fun setContent(content: @Composable () -> Unit) { 56 | shouldCreateCompositionOnAttachedToWindow = true 57 | this.content.value = content 58 | if (*isAttachedToWindow*) { 59 | createComposition() 60 | } 61 | } 62 | 63 | 64 | 65 | ## ComposeView? 66 | > A android.view.View that can host Jetpack Compose UI content. Use setContent to supply the content composable function for the view. 67 | 68 | android.view.View 는 Jetpack Compose UI 콘텐츠를 사용할 수 있도록 해줍니다. setContent 를 사용하면 composable function content 를 뷰에 제공할 수 있습니다. 69 | 70 | Compose 의 계층 구조는 아래와 같으며. ComposeView 를 통해 androidx.compose.materia 에 정의된 다양한 컴포넌트를 조합하여 Composable function 콘텐츠를 구성할 수 있습니다. 71 | 72 | ``` 73 | kotlin.Any 74 | ↳ android.view.View 75 | ↳ android.view.ViewGroup 76 | ↳ androidx.compose.ui.platform.AbstractComposeView 77 | ↳ androidx.compose.ui.platform.ComposeView 78 | ``` 79 | 80 | 81 | 82 | ## Compose Compiler? 83 | > Transform @Composable functions and enable optimizations with a Kotlin compiler plugin. 84 | 85 | Compose Compiler 는 [@Composable](http://twitter.com/Composable) annotation 이 설정된 경우 Composable function 으로 코드 변환과 코틀린 컴파일러 플러그인과 함께 최적화를 활성화합니다. 86 | 87 | ## Compose Runtime? 88 | > Fundamental building blocks of Compose’s programming model and state management, and core runtime for the Compose Compiler Plugin to target. 89 | 90 | Compose의 프로그래밍 모델과 상태 관리, 그리고 Compose 컴파일러를 지정하기 위한 코어 런타임에 대한 기본 설정을 합니다. 91 | 92 | 93 | 94 | ### **Compose compiler 에 의해 Composable 은 아래와 같이 변경됩니다.** 95 | 96 | ``` 97 | @Composable 98 | fun Greeting(name: String) { 99 | var greet by remember { mutableStateOf("Hello $name") } 100 | Text(text = greet, color = Color.Red) 101 | } 102 | ``` 103 | 104 | ``` 105 | fun Greeting( 106 | $composer: Composer, 107 | $static: Int, 108 | name: String 109 | ) { 110 | $composer.start(123) 111 | var greet by remember { mutableStateOf("Hello $name") } 112 | Text(text = greet, color = Color.Red) 113 | $composer.end() 114 | } 115 | ``` 116 | 117 | Compose 는 composer.start 에서 고유의 키를 가지고 있습니다. 이는 Compose 의 state 가 변경될 때 해당 키를 가진 Compose 만 변경되도록 동작합니다. static 은 상태(state)의 변경여부를 알 수 있는데 상태의 변화가 없는 경우, composer.start 와 composer.end 사이의 UI 의 변경을 하지 않습니다. 데이터의 상태가 변경되어 UI 를 다시 구성하는 경우는 Recomposition 이라고 합니다. 118 | 119 | 120 | 121 | 이 외에 Compose 에 더 자세한 내용은 사항은 아래 사이트에서 확인할 수 있습니다. 122 | 123 | 124 | [https://developer.android.com/jetpack/compose/state](https://developer.android.com/jetpack/compose/state) 125 | 126 | [https://www.youtube.com/watch?v=Q9MtlmmN4Q0](https://www.youtube.com/watch?v=Q9MtlmmN4Q0) 127 | 128 | [https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd](https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd) 129 | 130 | 131 | 132 | ## 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /002. Kotlin Coroutine/heesungbae.md: -------------------------------------------------------------------------------- 1 | ## 코루틴? 2 | ### 루틴(routine) 3 | - 컴퓨터 프로그램의 일부로서, 특정한 일을 실행하기 위한 일련의 명령. 4 | 5 | ### 코루틴(coroutine) 6 | - 여러개의 루틴들이 협력(co)한다는 의미. 7 | 8 | ### suspend 9 | - 실행 중 코루틴을 차단하지 않고 정지시킴. 10 | - 모든 로컬 변수를 저장함. 11 | 12 | ### resume 13 | - 정지된 위치부터 정지된 코루틴을 계속 실행. 14 | 15 | ### dispatchers - 코루틴 실행 위치를 지정. 16 | - `Dispatchers.Main` : Android UI 쓰레드 17 | - `Dispatchers.IO` : 입출력 쓰레드 18 | - `Dispatchers.Default` : 기본 쓰레드 19 | 20 | ### launch 21 | - 결과를 반환하지 않음, 실행 후 삭제되는 작업 22 | ### async 23 | - await 정지함수로 결과를 반환하는 작업 24 | 25 | ### runBlocking 26 | 코루틴을 실행하고 완료되기 전까지 현재 쓰레드를 블로킹. 27 | 28 | ### Flow 29 | 비동기식으로 계산할 수 있는 데이터 스트림 30 | 31 | - emit : 수동으로 데이터 스트림 Flow를 만듬 32 | - map : 데이터를 변환 가능함 33 | - collect : 스트림의 모든 값을 가져옴 34 | - flowOn : Flow의 Context 변경함 35 | - StateFlow : 식별 가능한 변경 가능 상태를 유지(항상 활성 상태) 36 | -------------------------------------------------------------------------------- /002. Kotlin Coroutine/kijunglee_pt1.md: -------------------------------------------------------------------------------- 1 | # (Android Deep Dive) Coroutines Part 1 2 | 3 | ## 프로세스, 스레드, 코루틴 4 | 5 | 우리가 코루틴을 알아보기 전, 먼저 프로세스가 무엇인지, 스레드가 무엇인지 인지를 하고 공부를 해보면 좋을것이다. 간략한 개념을 보도록하자. 6 | 7 | ### Process | 프로세스 8 | 9 | 프로세스는 컴퓨터에서 연속적으로 실행되고 있는 프로그램을 의미한다. 또한, 프로그램이 각 할당된 Heap 메모리에 적재되어 실행되는 인스턴스이기도하다. 10 | 11 | 프로세스의 특징적인 부분은 서로 완벽히 독립적인 공간을 가진다는 것이다. 프로세스는 각자의 스택과 데이터영역을 갖고, 보호받을 수 있다. 12 | 13 | 따라서, 한 프로세스가 예외적인 상황으로 종료되어도, 다른 프로세스에는 전혀 지장이없다. 14 | 15 | ### Thread | 스레드 16 | 17 | 쓰레드는 하나의 프로세스에 실행되는 여러 흐름의 단위이다. 쓰레드는 프로세스 내 별도의 메모리 영역을 가진다. 프로세스는 Heap 메모리에 할당되지만, 그 위에 Stack이라는 메모리 영역 위에 올라간다. 즉 100개의 쓰레드가 만들어지면, 100개의 스택 메모리에 각각 할당되는 것이다. 18 | 19 | 쓰레드는 본질적으로 프로세스 내 속해있기 때문에 쓰레드간 자원을 공유할 수 있다라는 특징이 있다. 20 | 21 | ## Concurrency & Parallelism 22 | 23 | 우리는 먼저 **Concurrency | 동시성**과 **Parallelism 병렬성**에 대해 알아볼 필요가 있다. 24 | 25 | ### Concurrency | 동시성 26 | 27 | 동시성은 여러개의 Task가 있는데, 여러개의 Task를 각각 쪼개서 실행할 수 있다. 예를 들면 A Task에 1분이 걸리고, B Task에 5분이 걸린다고 가정하자. 동시성 처리는 A Task와 B Task를 조금씩 나누어 처리하게 됨으로써 총 6분이 소요되게된다. 다만, 기존에 절차지향과 다르게 동시에 실행이된다라는 특징이 있다. 또한 Context Switching이 발생한다라는 특징이 있다. 28 | 29 | ### Parallelism | 병렬성 30 | 31 | Task 수 만큼 쓰레드가 필요하다. 쓰레드를 이용하게 되면 프로세스 내에서 자원이 공유되기 때문에 Context Switching이 필요하지않다. 따라서, 병렬적으로 실행되는 방식으로는 예를들어 A Task에 1분이 걸리고, B Task에 5분이 걸린다면, 총 5분이 걸리게 된다. 32 | 33 | ### Thread V.S. Coroutine 34 | 35 | 쓰레드와 코루틴은 둘다 동시성 (Interleaving) 를 보장하기 위한 기술이다. 여러개의 작업을 동시에 수행할 때 쓰레드는 각 작업에 해당하는 메모리를 할당하지만, 여러 작업을 동시에 실행하다보면 쓰레드 풀의 한정된 수에 따라 OS레벨에서 적절히 스케쥴링이 필요하다. 따라서, 쓰레드를 많이 생성하는 것은 그만큼 많은 비용을 생성한다. 그런 의미에서 코루틴은 최대한 처리 비용을 줄이면서, Context Switching 비용을 줄이기 위해 고안된 기술이다. **Light-Weight Thread**라 불릴만큼 동작하는 결과는 쓰레드와 유사하게 보이는데, 실제로 쓰레드를 생성하는 것이 아닌 Object를 할당해주는 방식이기 때문에 해당 Object를 스위칭하여 Context Switching 오버헤드를 줄였다. 36 | 37 | ## Hello Concurrent World! 38 | 39 | 각 언어별로 지원하는 코루틴 스킬이 다른데, **Java**의 경우 대표적으로 API 8의 Future가 대표적이고, **JavaScript**의 경우 Promise 등을 제공하여 실행을 관리한다. 40 | 41 | **Kotlin**의 경우 굉장히 쉽고 다양한 방식으로 동시성 프로그래밍을 제공한다. **Kotlin**의 코루틴도 여느 다른 언어들과 마찬가지로 언어레벨에서 키워드를 통해 제공한다. 42 | 43 | 코루틴을 이용하면 루틴을 진행하는 시점에 특정 상황에서 코루틴 **Scope | 범위**로 이동하게되고, 기존에 실행하고 있던 루틴을 정지하도록 만들 수 있다. 44 | 45 | 이를 위해서 Kotlin 코루틴에서는 약간의 핵심 키워드를 보면된다. 46 | 47 | 일단 코루틴을 쓰기이전에 우리가 어떻게 코드를 짜왔는지 비교해본다면, 코루틴은 비동기 프로그래밍에 단비같은 존재라는 것을 느낄 수 있다. 48 | 49 | ### Callback Code 50 | 51 | ![Callback Code](https://imgur.com/5KwV6fO.jpg) 52 | 53 | 그나마 Rx와 같은 비동기 프로그래밍 라이브러리를 사용하면, 훨씬 더 쉬운 방법으로 스트림을 제어할 수 있다. 54 | 55 | ### RxKotlin Code 56 | 57 | ![RxKotlin Code](https://imgur.com/2e5uFMY.jpg) 58 | 59 | 그럼에도 불구하고, 여전히 모나드라는 Data Hold Instance에 갇혀, 흐름제어를 한다는 것은 무언가 한꺼풀 신경써야 하는 부분이 생긴다. 60 | 61 | ### Coroutines Code 62 | 63 | 코루틴을 사용하면, 코드를 읽을 때 분명 순차적으로 코드 흐름이 실행되는 것으로 보이지만, 실제로는 비동기적으로 흐름을 제어할 수 있다. 64 | 65 | ![Coroutines Code](https://imgur.com/UEeP8UQ.jpg) 66 | 67 | ### Suspend - 일시 중단 함수 68 | 69 | ```kotlin 70 | suspend fun sum(val1: Int, val2: Int): Int { 71 | delay(2000) 72 | return val1 * val2 73 | } 74 | ``` 75 | 76 | **Kotlin** 코루틴은 언어레벨에서 공식적으로 지원하는 키워드이다. 이 키워드를 이용하였을 때 결과적으로 코루틴의 실행을 일시 중단 / 재개 할 수 있다. 77 | 78 | 한번 원리를 보도록 하자. 79 | 80 | **`suspend`**라는 키워드 하나를 붙였음에도 이 함수의 반환은 2초나 걸리게 된다. 내부적으로 어떤 마법이 일어나는 것일까? 81 | 82 | suspend가 붙은 함수는 컴파일러에서 특별한 처리가 이뤄지게 된다. 83 | 84 | ## CPS(Continuation Passing Style) 85 | 86 | ```kotlin 87 | fun postItem(item: Item) { 88 | val token = requestToken() // suspend function 89 | val post = createPost(token, item) // suspend function 90 | processPost(post) 91 | } 92 | 93 | suspend fun requestToken() { 94 | ... 95 | } 96 | 97 | suspend fun createPost(token: String, item: Item): Post { 98 | ... 99 | } 100 | ``` 101 | 102 | 다음과 같이 suspend function으로 구성되어 있는 함수를 요청하는 `postItem(Item)` 이라는 함수가 있다. 이함수를 컴파일러에서 컴파일 시 위코드는 CPS 방식으로 아래와 같이 변환되게 된다. 103 | 104 | ```kotlin 105 | fun postItem(item: Item) { 106 | requestToken { token -> 107 | val post = createPost(token, item) // Continuation 108 | processPost(post) // Continuation 109 | } 110 | } 111 | ``` 112 | 113 | CPS로 변환된 코드를 보면 콜백함수와 굉장히 유사한 형태를 띄는것을 알 수 있다. 114 | 115 | ### How does it works? 116 | 117 | 그렇다면, CPS로 변환된 코드가 눈으로 보았을 때는 순차적이지만, 어떻게 비동기적으로 동작하고 중단(suspend) 했다가 재개(resume)할 수 있는것일까? 118 | 119 | ```kotlin 120 | suspend fun createPost(token: String, item: Item): Post { ... } 121 | ``` 122 | 123 | 위와 같이 작성되었던 함수는 Reverse Compile하면 아래와 같이 Object로 반환을 하는 함수로 변환이 된다. 그리고 맨 마지막 매개변수에 Continuation이라는 타입의 객체가 인자로 추가된 것을 알 수 있는데, 이 객체가 흐름에 대한 제어를 할 수 있는 핵심요소이다. 124 | 125 | ```java 126 | Object createPost(Token token, Item item, Continuation cont) { ... } 127 | ``` 128 | 129 | ### Label 130 | 131 | ```kotlin 132 | suspend fun postItem(item: Item) { 133 | // LABEL 0 134 | val token = requestToken() 135 | // LABEL 1 136 | val post = createPost(token, item) 137 | // LABEL 2 138 | processPost(post) 139 | } 140 | ``` 141 | 142 | suspend 키워드가 붙은 함수는 컴파일되면서 다음과 같이 주석으로 LABEL ${index}가 추가되는데, 코루틴은 함수가 중지/재개될 수 있도록 LABEL을 통해 중단/재개 지점을 정한다. 143 | 144 | ```kotlin 145 | suspend fun postItem(item: Item) { 146 | switch (label) { 147 | case 0: 148 | val token = requestToken() 149 | case 1: 150 | val post = createPost(token, item) 151 | case 2: 152 | processPost(post) 153 | } 154 | } 155 | ``` 156 | 157 | 그리고, 위 코드는 LABEL이 다 추가되면 CPS 형태로 변환되면서 아래와 같이 변하게 된다. 158 | 159 | ```kotlin 160 | fun postItem(item: Item, cont: Continuation) { 161 | val sm = object: CoroutineImpl { ... } 162 | switch (sm.label) { 163 | case 0: 164 | requestToken(sm) 165 | case 1: 166 | createPost(token, item, sm) 167 | case 2: 168 | processPost(post) 169 | } 170 | } 171 | ``` 172 | 173 | **Continuation** 객체는 콜백 인터페이스 넘겨줌으로써 재개해주는 콜백 인터페이스다. 여기서 sm은 State Machine을 의미하는데, 각 함수를 호출할때는 지금까지 한 연산의 결과를 취합하여 넘겨준다. 그렇기에 코루틴으로 구성된 함수들은 sm이라는 변수 이름으로 매개변수를 넘겨받게 된다. 이러한 것을 우리는 Continuation이라고 하며, 어떤 정보 값을 가진 형태로 전달되어 174 | 175 | -------------------------------------------------------------------------------- /002. Kotlin Coroutine/meeting.md: -------------------------------------------------------------------------------- 1 | https://github.com/airbnb/mavericks 2 | - MVI 기반의 아키텍쳐 3 | - Mavericks is built on top of Android Jetpack and Kotlin Coroutines so it can be thought of as a complement rather than a departure from Google's standard set of libraries. 4 | 5 | --- 6 | 7 | TOC는 공식 docs의 예제는 불친절하다. 8 | 9 | 유명 오픈소스에서 corouines 사용 부분을 찾아서 예시로 드는 건 어떨까? 10 | 11 | --- 12 | 13 | https://myungpyo.medium.com/reading-coroutine-official-guide-thoroughly-part-0-20176d431e9d 14 | 15 | --- 16 | 17 | 코틀린 동시성 프로그래밍 목차를 기준으로, TOC를 잡고 살을 붙인다. 18 | 1. 공식 문서 19 | 2. 심명표님 medium 20 | 3. 기타 리서치 21 | - Sample : https://github.com/PacktPublishing/Learning-Concurrency-in-Kotlin 22 | - Android에서 다루는 corountines의 범위 안에서 컨텐츠를 덧붙인다. 23 | - https://developer.android.com/topic/libraries/architecture/coroutines 24 | - https://developer.android.com/kotlin/coroutines -------------------------------------------------------------------------------- /002. Kotlin Coroutine/namhoonkim.md: -------------------------------------------------------------------------------- 1 | 2 | ## (Android Deep Dive) Kotlin Coroutine 3 | 4 | ### Kotlin Coroutine이란? 5 | 6 | 원활한 비동기 프로그래밍 개발을 위하여 Kotlin은 언어 레벨에서 Coroutine 이라는 도구를 제공한다. 7 | 8 | Coroutine은 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴으로, Kotlin 버전 1.3에 추가되었으며 다른 언어에서 이미 확립된 개념을 기반으로 한다. 9 | 10 | Coroutine이라는 개념은 1963년에 이미 출판본에서 확인할 수 있으며, subroutine이나 thread 등과 비교한 글들도 쉽게 찾아 볼 수 있다. 11 | 12 | > **참고** [Design of a Separable Transition-Diagram Compiler(1963)](http://melconway.com/Home/pdf/compiler.pdf) 13 | 14 | ### Android의 비동기 프로그래밍 관점에서의 Coroutine 기능 15 | - **Lightweight** : 경량 16 | - 코루틴을 실행 중인 스레드를 차단하지 않는 정지를 지원하므로 단일 스레드에서 많은 코루틴을 실행할 수 있다. 17 | - 정지는 많은 동시 작업을 지원하면서도 차단보다 메모리를 절약할 수 있다. 18 | - **Fewer memory leaks** : 메모리 누수 감소 19 | - 구조화된 동시 실행을 사용하여 범위 내에서 작업을 실행한다. 20 | - **Built-in cancellation support** : 기본으로 제공되는 취소 지원 21 | - 실행 중인 코루틴 계층 구조를 통해 자동으로 취소가 전달된다. 22 | - **Jetpack integration** : Jetpack 통합 23 | - 많은 Jetpack 라이브러리에 코루틴을 완전히 지원하는 확장 프로그램이 포함되어 있다. 24 | - 일부 라이브러리는 구조화된 동시 실행에 사용할 수 있는 자체 코루틴 범위도 제공한다. 25 | 26 | ### Kotlin Basic 27 | 28 | #### launch 29 | 30 | coroutine을 이용해 비동기로 실행할 부분을 `launch` 블록으로 감싸면 비동기로 실행이 된다. 31 | 32 | ```kotlin 33 | import kotlinx.coroutines.* 34 | 35 | fun main() { 36 | GlobalScope.launch { // launch a new coroutine in background and continue 37 | delay(1000L) // non-blocking delay for 1 second (default time unit is ms) 38 | println("World!") // print after delay 39 | } 40 | println("Hello,") // main thread continues while coroutine is delayed 41 | Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive 42 | } 43 | ``` 44 | 45 | 출력 결과는 아래와 같다. 46 | 47 | ``` 48 | Hello, 49 | World! 50 | ``` 51 | 52 | 이 `launch`는 `job`이라는 객체를 반환하는데 이를 통해, 비동기 코드의 완료를 명시적으로 기다리거나 중단시킬 수 있다. 53 | 54 | ```kotlin 55 | val job = GlobalScope.launch { // launch a new coroutine and keep a reference to its Job 56 | delay(1000L) 57 | println("World!") 58 | } 59 | println("Hello,") 60 | job.join() // wait until child coroutine completes 61 | ``` 62 | 63 | #### runBlocking 64 | 65 | `runBlocking`을 사용하면 jvm의 matin thread를 블록내의 코드를 다 수행할때까지 block시킨다. 66 | 67 | ```kotlin 68 | import kotlinx.coroutines.* 69 | 70 | fun main() { 71 | GlobalScope.launch { // launch a new coroutine in background and continue 72 | delay(1000L) 73 | println("World!") 74 | } 75 | println("Hello,") // main thread continues here immediately 76 | runBlocking { // but this expression blocks the main thread 77 | delay(2000L) // ... while we delay for 2 seconds to keep JVM alive 78 | } 79 | } 80 | ``` 81 | 82 | 출력 결과는 아래와 같다. 83 | 84 | ``` 85 | Hello, 86 | World! 87 | ``` 88 | 89 | #### coroutineScope 90 | 91 | `GlobalScope.launch`만으로 작업한다면 job에 대한 join을 관리하면서 오류가 발생할 확률이 높다. 92 | 93 | 이를 방지하기 위해 Scope 내부에서 또 다른 Scope를 생성할 수 있다. 94 | 95 | Coroutine 외부에 존재하는 블럭은 내부에서 실행되는 모든 coroutine이 종료되어야 실행된다. 96 | 97 | ```kotlin 98 | import kotlinx.coroutines.* 99 | 100 | fun main() = runBlocking { // this: CoroutineScope 101 | launch { 102 | delay(200L) 103 | println("Task from runBlocking") 104 | } 105 | 106 | coroutineScope { // Creates a coroutine scope 107 | launch { 108 | delay(500L) 109 | println("Task from nested launch") 110 | } 111 | 112 | delay(100L) 113 | println("Task from coroutine scope") // This line will be printed before the nested launch 114 | } 115 | 116 | println("Coroutine scope is over") // This line is not printed until the nested launch completes 117 | } 118 | ``` 119 | 120 | 출력 결과는 아래와 같다. 121 | 122 | ``` 123 | Task from coroutine scope 124 | Task from runBlocking 125 | Task from nested launch 126 | Coroutine scope is over 127 | ``` 128 | 129 | #### suspend 130 | Coroutine에서 사용되는 코드들을 외부로 빼려면 `suspend` 키워드를 사용한다. 131 | 132 | `suspend`로 지정된 메서드는 내부에서 Coroutine api를 호출할 수 있게 된다. 133 | 134 | ```kotlin 135 | import kotlinx.coroutines.* 136 | 137 | fun main() = runBlocking { 138 | launch { doWorld() } 139 | println("Hello,") 140 | } 141 | 142 | // this is your first suspending function 143 | suspend fun doWorld() { 144 | delay(1000L) 145 | println("World!") 146 | } 147 | ``` 148 | 149 | 출력 결과는 아래와 같다. 150 | 151 | ``` 152 | Hello, 153 | World! 154 | ``` 155 | 156 | 157 | --- 158 | 159 | 160 | 161 | - async 162 | 163 | ### DLC 164 | - iterator 165 | - sequence 166 | - Channel 167 | - Flow 168 | 169 | 170 | ## References 171 | https://kotlinlang.org/docs/coroutines-overview.html 172 | https://developer.android.com/kotlin/coroutines 173 | 174 | 175 | 176 | 177 | --- 178 | -------------------------------------------------------------------------------- /002. Kotlin Coroutine/seheejeong-2.md: -------------------------------------------------------------------------------- 1 | ## 👏코루틴의 동시성 내부 2 | `suspend function`은 `CPS`로 수행된다. 이는 호출되는 함수에 continuation을 보내는 것을 전제로 하고있어, 함수가 완료되는대로 continuation을 계속 호출할 것이다.(이를 callback으로도 생각할 수 있다.) suspend function이 다른 suspend function을 호출할 때마다 완료 혹은 오류가 발생했을 때 호출되어야 하는 continuation을 전달하게 된다. 3 | suspend function은 상태 머신으로 변환되는데 상태를 저장하고 복구하며 한번에 코드의 한 부분을 실행하게 된다. 그래서 suspend function을 재개할 때마다 중단된 위치에서 상태를 복구하고 실행을 지속할 수 있는 것이다. CPS와 상태머신이 결합하면 컴파일러는 다른 연산이 완료되기를 기다리는 동안 suspend 가능한 function을 생성하게 된다. 4 | 5 | ### Continuation 6 | 모든 것은 suspend function의 빌딩 블록이라고 볼 수 있는 `연속체(continuation)`에서부터 시작하게 된다. 이 연속체는 코루틴을 재개할 수 있는 중요한 인터페이스이다. 7 | 8 | ``` kotlin 9 | interface Continuation { 10 | val context: CoroutineContext 11 | 12 | // 일시 중단을 일으킨 작업의 결과이다. 13 | // 해당 함수가 Int 를 반환하는 함수를 호출하기위해 일시중지가 되면, T 값은 Int 가 된다. 14 | fun resume(value: T) 15 | 16 | // 예외를 전파할 때 사용된다. 17 | fun resumeWithExceptioin(exception: Throwable) 18 | } 19 | ``` 20 | 21 | ### suspend 22 | 코루틴에서는 `suspend`라는 한정자를 추가해서, 주어진 범위의 코드가 연속체를 사용해 동작하도록 컴파일러에게 지시할 수 있도록 만들어졌다. 그래서, suspend function이 컴파일 될 때마다 바이트코드가 하나의 커다란 연속체가 된다. 23 | 24 | ``` kotlin 25 | suepend function getUserSummary(id: Int): UserSummary { 26 | logger.log("fetching summary of $id") 27 | val profile = fetchProfile(id) // suspend function 28 | val age = calcuateAge(profile.dateOfBirth) 29 | val terms = validateTerms(profile.country, age) //suspend function 30 | return UserSummary(profile, age, terms) 31 | } 32 | ``` 33 | suspend function 인 `getUserSummary()`은 실행을 제어하기 위해 연속체를 사용할 것이며, 두 번의 일시중지가 이루어질 것이다. 34 | 35 | 36 | 37 | ## 👏상태 머신 38 | 컴파일러가 위의 suspend function 코드를 분석하는 방식에 대해 알아보자. 39 | 40 | ### 1. 라벨 41 | 컴파일러는 실행이 시작되는 부분과 실행이 재개될 수 있는 부분에 라벨을 포함하게 된다. 42 | 43 | ``` kotlin 44 | suepend function getUserSummary(id: Int): UserSummary { 45 | // label 0 -> 첫 번째 실행 46 | logger.log("fetching summary of $id") 47 | val profile = fetchProfile(id) // suspend function 48 | 49 | // label 1 -> resume 50 | val age = calcuateAge(profile.dateOfBirth) 51 | val terms = validateTerms(profile.country, age) //suspend function 52 | 53 | // label 2 -> resume 54 | return UserSummary(profile, age, terms) 55 | } 56 | ``` 57 | 이는 when 구문으로 분리되어 표현된다. 58 | 59 | ```kotlin 60 | when(label) { 61 | 0 -> { 62 | logger.log("fetching summery of $id") 63 | fetchProfile(id) 64 | return 65 | } 66 | 67 | 1 -> { 68 | calculateAge(profile.dateOfBirth) 69 | validateTerms(profile.country, age) 70 | return 71 | } 72 | 73 | 2-> UserSummary(profile, age, terms) 74 | } 75 | ``` 76 | 77 | ### 2. 연속체 78 | 다른 지점에서 실행을 재개할 수 있도록 코드가 변환이 되었다면, 이제 함수의 라벨로 어떻게 도달해야하는지 찾아야 한다. 79 | 라벨의 속성을 포함하고 있는 연속체의 추상체를 구현하고 있는 `CoroutineImpl` 를 구현해보자. 80 | 81 | ```kotlin 82 | suspend fun getUserSummary(id: Int, cont: Continuation): UserSummary { 83 | val sm = object : CoroutineImpl { 84 | override fun doResume(data: Any?, exception: Thowable?) { 85 | getUserSummary(id, this) 86 | } 87 | 88 | val state = sm as CoroutineImpl 89 | when(state.label) { 90 | .... 91 | } 92 | } 93 | ``` 94 | `doResume()` 이 `getUserSummary()`로 콜백을 전달할 수 있게 된다. 호출하는 쪽이, 중단과 재개도 같이 이루어져야하기 때문에 resume에서 `getUserSummary()`가 완료되었을 때 cont 만을 인자로 받는 것이다. 95 | 96 | ### 3. 콜백 & 라벨 증가 97 | 라벨을 이용해 특정 시점에서 재개가 가능하게되었다면, `getUserSummary()` 로부터 호출된 다른 suspend function 이 CoroutineImple를 전달받을 수 있어야 한다. 98 | ``` kotlin 99 | when(state.label) { 100 | 0 -> { 101 | logger.log("fetching summery of $id") 102 | sm.label = 1 103 | fetchProfile(id, sm) 104 | return 105 | } 106 | 107 | 1 -> { 108 | calculateAge(profile.dateOfBirth) 109 | sm.label = 2 110 | validateTerms(profile.country, age, sm) 111 | return 112 | } 113 | 114 | 2-> UserSummary(profile, age, terms) 115 | } 116 | ``` 117 | `fetchProfile()` 과 `validateTerms()`가 `Continuation` 를 수신하고, 실행이 완료되는 시점에 `doResume()`이 호출되도록 수정되었다. 그러면, 이 두개의 함수가 실행이 끝날 때마다 수신하는 연속체를 호출하게 되는데, `getUserSummary()`에 구현된 연속체에서 실행을 재개할 수 있다. 118 | 119 | 또한 다른 suspend function이 호출되기전에 라벨을 증가시켜 다른 함수를 호출시킬 수 있도록 만든다. -------------------------------------------------------------------------------- /003. View Rendering And Optimization/heesungbae.md: -------------------------------------------------------------------------------- 1 | ## UI 렌더링 성능 개선 2 | --- 3 | 4 | ### 레이아웃 계층 구조 최적화 5 | ![image](https://miro.medium.com/max/692/1*abc0UlGj1myFD0eph4pZjQ.png) 6 | - 레이아웃은 View와 ViewGroup으로 이뤄진다 7 | - RootView를 시작으로 트리 구조로 뷰를 생성한다 8 | - View의 중첩이 많아질수록 뷰를 그리는 시간이 증가한다 9 | - requestLayout() 함수를 적게 사용한다 10 | - invalidate() : text, color 변경, 뷰가 터치되었을 때 onDraw() 함수를 재호출 11 | - requestLayout() : 뷰의 사이즈가 변경되었을 때 onMeasure() 함수를 재호출 -> 시간이 오래걸림 12 | 13 | ### 레이아웃 재사용 14 | - include, merge를 통해 뷰를 재사용한다 15 | 16 | - include로만 뷰를 재사용하면 다음과 같이 뷰가 중첩될 수 있다 17 | ![before-merge](https://miro.medium.com/max/1000/1*Grxj64w7gmVrJxDsk4qDcA.png) 18 | 19 | - merge를 같이 사용하여 중첩을 줄여준다 20 | ![before-merge](https://miro.medium.com/max/1000/1*FyzCjMY3e8eUMHzbobRTzg.png) 21 | 22 | 23 | ## 렌더링 최적화 기법 24 | --- 25 | 26 | ### 오버드로잉 방지(백그라운드) 27 | 보통 앱의 배경 색상을 적용하기 위해서 `android:background="@color/white"` 해당 뷰의 배경색상을 적용한다 28 | ```xml 29 | 30 | 31 | 38 | 39 | 47 | 48 | 49 | ``` 50 | 51 | 허나 이렇게 배경색을 지정하면 아래과 같이 Overdraw가 생긴다 52 | 53 | ![example1](https://i.imgur.com/Sfjx4BJ.png) 54 | 55 | 왜 Overdraw가 생기는지 확인을 위해 앱 테마 값을 확인해보았다. 56 | ```xml 57 | 58 | 59 | 60 | 73 | 74 | ``` 75 | 76 | 특별히 지정해준 사항이 없어 해당 테마의 Parent를 따라가 보았다 77 | ``` 78 | Theme.MaterialComponents.DayNight.DarkActionBar > 79 | Theme.MaterialComponents.Light.DarkActionBar > 80 | Base.Theme.MaterialComponents.Light.DarkActionBar > 81 | Base.Theme.MaterialComponents.Light > 82 | Base.V14.Theme.MaterialComponents.Light 83 | ``` 84 | 85 | 해당 소스에서 아래와 같이 배경 색상을 지정하는 걸 확인하였고 86 | ```xml 87 | @color/design_default_color_background 88 | ``` 89 | 90 | 해당 값을 확인해보니 아래와 같이 흰색으로 설정하고 있었다 91 | ```xml 92 | #FFFFFF 93 | ``` 94 | 95 | 조금 자세한 사항 확인을 위해 `Layout Inspector`로 세부 뷰를 살펴보니 96 | 아래와 같이 `DecorView`와 `ConstraintLayout` 모두 배경색이 칠해져 있는걸 확인했다 97 | 98 | ![withBackground](https://i.imgur.com/9ghKD7B.png) 99 | 100 | 101 | `ConstraintLayout`의 Overdraw를 없애고 내가 원하는 색상으로 설정하기 위해 테마 파일에 102 | `@color/beige` 코드를 입력하였다 103 | 104 | ```xml 105 | 106 | 107 | 108 | 122 | 123 | ``` 124 | 비교를 위해 `/res/layout/activity_main.xml` 파일에 `android:background="@color/beige"` 코드를 입력 후 Overdraw를 확인하였다 125 | 126 | ![example2](https://i.imgur.com/s5LAvBX.png) 127 | 128 | OverDraw는 확실히 줄어든걸 확인 할 수 있었고 `Layout Inspector`로 세부 뷰를 살펴보니 129 | 아래와 같이 `ConstraintLayout`의 배경색 없어지고 `DecorView`의 배경색만 남아 있는 것을 확인할 수 있었다 130 | 131 | ![withoutBackground](https://i.imgur.com/hXAo8w0.png) 132 | 133 | 배경 색상 지정을 테마를 통해 지정하면 Overdraw를 줄일 수 있다 134 | 135 | 136 | 137 | --- 138 | ### Reference 139 | - [https://proandroiddev.com/writing-performant-layouts-3bf2a18d4a61](https://proandroiddev.com/writing-performant-layouts-3bf2a18d4a61) 140 | - [https://jungwoon.github.io/android/2019/10/02/How-to-draw-View.html](https://jungwoon.github.io/android/2019/10/02/How-to-draw-View.html) 141 | - [https://android-developers.googleblog.com/2009/03/window-backgrounds-ui-speed.html](https://android-developers.googleblog.com/2009/03/window-backgrounds-ui-speed.html) -------------------------------------------------------------------------------- /003. View Rendering And Optimization/namhoon.md: -------------------------------------------------------------------------------- 1 | ## Optimize UI rendering - View Rendering 2 | 3 | Android의 근간인 GUI. 4 | 5 | 이 GUI를 구성하는 화면 구성 요소의 기본이 바로 View이다. 6 | 7 | View는 크게 두 가지로 구분할 수 있다. 8 | 9 | 먼저 눈에 보이는 Widget. 그리고 눈에 보이지않는 Layout이 그것이다. 10 | 11 | 이 GUI를 구성하는 View가 어떻게 렌더링 되는 지 알아보자. 12 | 13 | >>>> 14 | Ref 15 | - https://developer.android.com/reference/android/view/View 16 | - https://developer.android.com/training/custom-views/create-view 17 | >>> 18 | 19 | ### LifeCycle 20 | 21 | 당연하겠지만 View도 Lifecycle을 가지고 있다. 22 | 23 | >>> 24 | LifeCycle 이미지 추가 25 | >>> 26 | 27 | >>> 28 | Ref 29 | - https://codentrick.com/android-view-lifecycle/ 30 | - https://medium.com/@sahoosunilkumar/understanding-view-lifecycle-in-android-e42890aab16 31 | >>> 32 | 33 | ### View를 그리는 방법 34 | 35 | >>>> 36 | Ref 37 | - https://developer.android.com/training/custom-views/create-view 38 | 39 | 1. 코드로 작성하는 법 40 | 2. xml로 작성하는 법 41 | 성능 차이가 있을까? 42 | 컴파일된 입장에서 이미 거의 없을텐데? 43 | - https://stackoverflow.com/questions/54032921/performance-android-views-generated-programmatically-vs-xml-views 44 | >>> 45 | 46 | ### Rendering 47 | 48 | Android에서의 Screen Layer를 명세해야할 필요가 있다. 49 | 50 | 추상화 Level 1 51 | Kernel -> Framework -> Application -> User 순서 52 | 53 | 추상화 Level 2 54 | SurfaceFlinger 55 | - BufferQueue / SurfaceConstrol or ASurfaceControl 56 | 57 | WindowManager 58 | - get Layer from SurfaceFlinger 59 | - Layer = SurfaceControl(BufferQueue + Surface + Meta data(+ Diplay Frame...)) 60 | 61 | ASurfaceControl 62 | - ASurfaceControl 63 | - ASurfaceTransaction 64 | - ASurfaceTransactionStats 65 | 66 | WindowManager 67 | - Window Control 68 | - Window Object = View's Container 69 | 70 | >>> 71 | Ref 72 | - https://source.android.com/devices/graphics/surfaceflinger-windowmanager?hl=ko 73 | - Surface + SurfaceHolder = https://source.android.com/devices/graphics/arch-sh 74 | - Canbas Rendering : Skia가 여기서 등판 75 | - OpenGL ES or Vulkan 76 | >>> 77 | 78 | ## Optimize UI rendering - How to Optimize? 79 | 80 | View를 그리는 것의 최적화는 두 가지 방향성이 있다고 볼 수 있다. 81 | 82 | 1. 그려지는 View 자체를 최적화하는것 83 | 2. View를 렌더링하는 프로세스를 최적화하는 것 84 | 85 | 각각 분리해서 알아보자. 86 | 87 | ### 그려지는 View 자체를 최적화하는것 88 | 89 | 1. Optimizing Layout Hierarchies (Hierarchy Viewer / Lint) 90 | 2. weight 속성 최소화 91 | 3. ``와 `` 활용 92 | 4. 무의미한 background 제거 93 | 5. Linear in Linear -> Relative로 전환 94 | 95 | >>> 96 | Ref 97 | - https://developer.android.com/training/improving-layouts/optimizing-layout 98 | - https://developer.android.com/topic/performance/rendering/optimizing-view-hierarchies?hl=ko 99 | - https://developer.android.com/training/improving-layouts/loading-ondemand 100 | >>> 101 | 102 | ### View를 렌더링하는 프로세스를 최적화하는 것 103 | 104 | 1. Lazy Load by ViewStub 105 | 2. Inspect GPU rendering speed and overdraw 106 | 3. 커스텀 뷰의 `onDraw()` 미사용시 clip과 reject 분리 107 | 108 | >>> 109 | Ref 110 | - https://developer.android.com/topic/performance/rendering/inspect-gpu-rendering 111 | - https://developer.android.com/reference/android/graphics/Canvas.html 112 | - https://stuff.mit.edu/afs/sipb/project/android/docs/training/custom-views/optimizing-view.html 113 | >>> 114 | -------------------------------------------------------------------------------- /003. View Rendering And Optimization/sehee.md: -------------------------------------------------------------------------------- 1 | ## 👏 UI는 어떻게 생성될까? 2 | 우리가 사용하는 `상위 수준의 객체` 들(Button, TextView, etc.)은 아래의 과정을 거쳐 화면의 픽셀로 나타낼 수 있다. ![](https://images.velog.io/images/jshme/post/b2efded5-296a-403c-8ca4-8f0bc62d055b/image.png) 3 | * CPU Level에서 상위 수준의 객체들을 가져오고, 이 객체를 그리기 위한 집합인 `displaylist` 로 바꾸어준다. 4 | * GPU는 높은 수준의 객체들을 가져와 텍스처 또는 화면에서 픽셀로 바꾸어주는 `Rasterization` 기능을 수행한다. CPU에서 만들어진 명령어들은 OpenGL-ES api를 이용해 GPU에 업로드 되며, 픽셀화에 이용된다. 한번 업로드되면 displaylist 객체는 캐싱되는데, 이는 우리가 다시 displaylist를 통해 그리고 싶다면 다시 새로 생성할 필요없이 기존에 존재하는 것을 다시 그리기 때문에 비용이 훨씬 더 저렴하다. 5 | 6 | UI 렌더링이 진행될 때, CPU에서 displaylist 를 생성하고 GPU에 업로드 하는 과정은 매우 고비용적이기 때문에, 효율적인 개선안을 찾아야한다. 7 | 8 | 9 | ### Displaylist 란 ? 10 | ![](https://t1.daumcdn.net/cfile/blog/24147F3558C5B7AB06?original) 11 | 안드로이드 3.0 이전에는 View의 변경사항이 일어나면, ViewRoot까지 전파하도록 설계되었다. View에서 ViewGroup, ViewRoot를 거쳐 다시 View 까지 내려와야하는 과정이 많은 코드를 접근하기 때문에 비효율적이었다. 12 | 13 | 이러한 대안에 Displaylist가 나오게 되었는데, 이는 UI 요소를 그리기 위한 정보를 리스트로 모아둔 것이다. UI요소의 변경이 일어났을 때, 다시 View tree 를 탈 필요가 없이 리스트를 확인하고 사용하면 된다는 이점이 있다. 14 | 15 | 16 | 17 | ## 👏 UI 렌더링 과정에 대해 이해해보자 18 | View가 렌더링 될 때, 상위수준의 `ViewGroup` 에서부터 하위 자식인 `View` 로 내려가면서 `Measure -> Layout -> Draw` 를 거치게 된다. 19 | 20 | ![](https://images.velog.io/images/jshme/post/384fceb5-01d8-4b9a-8241-d509eedda458/image.png) 21 | 22 | 1. Measure - ViewGroup와 View들의 크기를 측정하기 위해 View Tree를 top-down 형식으로 탐색을 완료한다. ViewGroup이 측정되면, 하위속성들도 측정한다. 23 | 24 | * View 의 크기는 2가지로 정의될 수 있다. 25 | 26 | 1. measured width & measured height : 뷰가 부모뷰 크기의 범위 내에서 가지고 싶어하는 너비와 높이이다. 27 | 2. drawing width & drawing height : 뷰의 실제 너비와 높이로 뷰를 그리기 위해서 사용한다. 28 | 29 | 30 | 31 | 32 | ✅ View의 Padding, Margin 등을 고려하면 원하는 크기에서 Padding 및 Margin 값을 빼야 하기 때문에 Measured Width, Measured Height 는 Drawing Width, Drawing Height 와 다를 수 있다. 33 | 34 | 2. Layout - 이 과정에서도 top-down 형식의 탐색이 일어나게 되는데, 이 때는 각각의 ViewGroup이 Measure 단계에서 측정된 크기를 이용해서 하위속성들의 위치를 결정한다. 35 | 36 | 3. Draw - GPU에게 명령을 내려주는 단계이다. 그려야 할 객체들의 리스트를 GPU에게 보내기 위해 View Tree에서 각 객체의 대한 Canvas 객체가 생성된다. 이 때, 이전 1,2단계를 거쳐 결정된 객체들의 크기와 위치에 대한 정보가 들어가게 된다. 37 | 38 | 만약 View가 변할 때, 시스템에게 알려주기 위한 2가지 방법이 존재하는데, `Invaliadate()`가 호출 될 때에는 draw부터 다시 작업이 시행되고, `requestLayout()`이 호출될 때에는 measure -> layout -> draw 단계를 다시 거치게 된다. 39 | 40 | > **번외)** 41 | > 42 | > **1. layout_weight 의 배신** 43 | > 44 | > Linear Layout 의 layout_weight 속성을 사용하는 경우 자식 뷰는 두번의 Measure pass가 필요하기 때문에 많은 비용이 소모된다. layout_weight는 단순히 비율을 나누어 공간을 차지하는 것이 아닌, 부모의 View가 그려지고 나서 남은 공간이 얼마만큼인지, 다른 View들이 그려지고 나서 다시한번 남은공간도 계산하고 나서 자기 자신을 그리기 때문에 지속적인 계산이 일어나게 된다. 45 | > 46 | > **2. Overdraw 를 피하는 방법** 47 | > 48 | > OverDraw를 피하는 방법 중, **사용자에게 보여지지 않는 Layout의 Background 색을 제거**하면 성능 향상에 도움이 된다는 글이 많이 존재한다. 하지만 어째서 배경 색을 지우는 것만으로도 성능이 크게 향상될 수 있다는 것일까? Layout에 Background를 제거한 후 디버깅을 해보면 View **Lifecycle의 onDraw를 거치지 않음**을 알 수 있다. UI 렌더링 시 가장 비용이 많이 드는 부분이 onDraw(GPU에 업로드 하는 과정) 인데, 이 부분이 skip 되니 성능이 향상되는건 당연한 부분일 것이다. 🙂 49 | 50 | 51 | 52 | 53 | ## 👏 Slow Rendering 54 | 55 | 56 | 16ms안에 화면을 갱신하면 사용자에게 있어 자연스러운 화면전환을 제공할 수 있기 때문에, 초당 60fps 안에 draw가 완료되어야 한다. 57 | 58 | 16ms 라는 수치는 정해진 수치이며, View 렌더링에 사용되는 `Choreographer` class에서 frame rate를 제어할 수 있도록 구성되어있다. 59 | 60 | ``` java 61 | public final class Choreographer { 62 | private static float getRefreshRate() { 63 | DisplayInfo di = DisplayManagerGlobal.getInstance().getDisplayInfo( 64 | Display.DEFAULT_DISPLAY); 65 | return di.refreshRate; 66 | } 67 | } 68 | 69 | /** 70 | * Describes the characteristics of a particular logical display. 71 | * @hide 72 | */ 73 | public final class DisplayInfo implements Parcelable { 74 | /** 75 | * The refresh rate of this display in frames per second. 76 | *

77 | * The value of this field is indeterminate if the logical display is presented on 78 | * more than one physical display. 79 | *

80 | */ 81 | public float refreshRate; 82 | } 83 | 84 | final class VirtualDisplayAdapter extends DisplayAdapter { 85 | private final class VirtualDisplayDevice extends DisplayDevice implements DeathRecipient { 86 | @Override 87 | public DisplayDeviceInfo getDisplayDeviceInfoLocked() { 88 | if (mInfo == null) { 89 | mInfo = new DisplayDeviceInfo(); 90 | mInfo.name = mName; 91 | mInfo.uniqueId = getUniqueId(); 92 | mInfo.width = mWidth; 93 | mInfo.height = mHeight; 94 | mInfo.refreshRate = 60; 95 | /*** Part of the code is omitted ***/ 96 | } 97 | return mInfo; 98 | } 99 | } 100 | } 101 | ``` 102 | 103 | 104 | 105 | 만약 16ms 이상 걸리게 된다면 어떻게 될까? View 렌더링 과정이 지체되어 사용자 경험에 좋지 않을 것이고, 애니메이션이 매끄럽게 보이지 않으며, 앱이 정상적으로 작동되지 않는 것처럼 보이게 될 것이다. 흔히 "렉걸린다" 라는 표현을 주로 하는데, 이를 `Frame Drop` 이라고 부른다. `Frame Drop` 이 발생하는 원인은 오랜 시간동안 뷰의 계층구조를 새로 그리거나, 유저가 볼 수 없는 화면에 공을 들여 색칠하는 경우 CPU & GPU 에 과부하를 일으키게 된다. 106 | 107 | 108 | 109 | > ![](https://images.velog.io/images/jshme/post/69d9cb45-6fdf-4715-acfa-3605901e381d/image.png) 110 | > 111 | > 112 | > 113 | > 개발자 옵션에서 볼 수 있는 `GPU 렌더링 프로파일` 에서 녹색 라인이 16ms 를 나타내는데, 이를 넘지 말라는게 위의 이유 때문이다. -------------------------------------------------------------------------------- /003. View Rendering And Optimization/siyoung_Profiling.md: -------------------------------------------------------------------------------- 1 | ## Profiling GPU Rendering 2 | 3 | 안드로이드에는 UI 를 필요이상으로 렌더링하거나 스레드와 GPU 연산을 오래 실행하는 앱에서 발생할 수 있는 이슈를 시각화하여 렌더링과 최적화를 도울수 있는 개발자 툴이 있다. 프로파일을 Android 4.1(API 16 ) 이상에서 사용 가능하며 미리 해당 기능을 활성화하는 작업이 필요하다. 다음과 같은 순서로 진행하면 활성화 할 수 있다. 4 | 5 | 1. 설정에서 개발자 옵션을 선택 6 | 2. 모니터링 섹션에서 Profile GPU Rendering 또는 Profile HWUI rendering 선택 7 | 3. 화면에 막대로 표시를 선택 8 | 9 | Profile GPU Rendering 활성화되면 아래와 같이 표시된다. 10 | 11 | ![](https://cdn-images-1.medium.com/max/2000/1*pMOgwlRuRGRubD6y5PKrlQ.png) 12 | 13 | * 가로축에 각 세로 막대는 하나의 프레임을 나타내며, 각각의 세로 막대의 높이는 하나의 프레임을 렌더링하는데 걸린 시간을 나타낸다. 14 | 15 | * 중간에 가로로 된 녹색 가로선은 16ms(milliseconds) 을 나타내며, 초당 60 프레임을 지원하려면 세로 막대는 녹색 가로선 아래에 유지되도록 해야한다. 이 녹색 가로선을 초과하는 경우에는 애니메이션이 일시 중단될 수도 있다. 이 외 노란 가로선은 21ms (47 fps) 빨간 가로선은 31ms (32 fps) 을 나타낸다. 16 | 17 | ![](https://cdn-images-1.medium.com/max/2000/0*rJMnRq6DAji2Xnul.png) 18 | 19 | 세로 막대 차트는 위와 같이 8개의 단계로 표시된다. 20 | 21 | * **프로세스/스왑버퍼(Swap)**: 안드로이드는 디스플레이 목록을 GPU 에 제출하는것을 끝내면, 시스템은 그래픽 드라이버에 현재 프레임이 완료되었음을 알리며 드라이버를 업데이트된 이미지를 화면에 표시한다. CPU 와 GPU 는 병렬로 처리되는데 CPU 가 GPU 처리보다 빠른 경우, 프로세스간 통신 큐가 가득찰 수 있다. CPU 는 큐에 공간이 생길때까지 기다린다. 큐가 가득한 상태는 스왑버퍼 상태에서 자주 발생하는데 이 단계에서 프레임 전체의 명령어가 제출되기 때문이다. 명령어 실행 단계와 비슷하게 GPU 에서 일어나는 일의 복잡성을 줄여 문제를 해결할 수 있다. 22 | 23 | * **명령어 실행(Issue)**: 디스플레이 목록을 화면에 그리기 위한 모든 명령어를 실행하기 위해 걸리는 시간을 측정한다. 주어진 프레임에서 렌더링하는 디스플레이 목록과 수량을 직접적으로 측정한다. 내재적으로 많은 그리기 동작이 있는 경우, 오랜 시간이 걸릴수 있다. 24 | 25 | * **동기화/업로드(Upload)**: 현재 프레임에서 비트맵 객체가 CPU 메모리에서 GPU 메모리로 전송되는데 걸리는 시간을 측정한다. 오랜 시간이 걸리는 경우는 작은 리소스가 많이 로드되어 있거나 적지만 큰 리소스가 로드되어 있는 경우이다. 비트맵 해상도가 실제 디스플레이 해상도보다 큰 사이즈로 로드되어 있지 않도록 하거나 동기화 전에 비동기로 미리 업로드해서 실행 시간을 줄일 수 있다. 26 | 27 | * **그리기(Draw)**: 백그라운드 혹은 텍스트 그리기와 같은 뷰를 그리기 명령어로 번역하는 단계로 시스템은 명령어를 디스플레이 목록에 캡처한다. 무효화된 뷰에서 onDraw 호출을 실행해 걸리는 모든 시간을 측정하며, 오랜 시간이 걸린다는 것은 무효화된 뷰가 많다는 것을 의미한다. 뷰가 무효화되는 경우 뷰의 디스프레이 목록을 재생성한다. 혹은 커스텀한 뷰가 onDraw 할때 복잡한 로직을 가질 때도 실행시간이 오랜 걸린다. 28 | 29 | * **측정 및 배치(Measure)**: 안드로이드에서 뷰를 스크린에 그리기 위해 측정하고 배치하는데 걸리는 시간을 측정한다. 일반적으로 오랜 시간이 걸리는 경우는 배치할 뷰가 너무 많거나 혹은 계층구조의 잘못된 장소에서 중복 계산이 이뤄지며, Traceview와 Systrace 를 사용해서 호출 스택을 확인하여 문제를 파악할 수 있다. 30 | 31 | * **애니메이션(Anim)**: 현재 프레임에서 실행되는 애니메이션에 대해 걸리는 시간을 측정한다. 일반적으로 오랜 시간이 걸리는 경우는 애니메이션의 속성 변경으로 인해 발생한다. 32 | 33 | * **입력 처리(Input)**: 앱이 입력 이벤트를 처리하는 시간으로 입력 이벤트 콜백의 결과로 호출된 코드를 실행하는데 걸리는 시간을 나타낸다. 일반적으로 오랜 시간이 걸리는 경우는 메인쓰레드에서 발생하며, 작업을 최적화하거나 다른 쓰레드를 이용해서 작업을 실행하도록 한다. 34 | 35 | * **기타(Misc)**: 렌더링 시스템이 작업을 수행하는 시간외에 렌더링과 관련 없는 추가적인 작업이 있다. 일반적으로 렌더링의 연속된 두 프레임 사이에서 UI 스레드에서 발생할 수 있는 일을 나타낸다. 오랜 시간이 걸리는 경우, 다른스레드에서 실행해야할 콜백, 인텐트 또는 기타 다른 작업이 있을 수 있다. Method tracing 또는 Systrace 를 사용하면 메인스레드에서 실행중인 작업을 확인해 문제를 해결할 수 있다. 36 | 37 | 38 | 39 | 아래에는 이미지 뷰에 클릭 이벤트를 설정하고 이미지 리소스를 변경하는 로직이다. 40 | 좌측에는 단순하게 이미지 리소스를 변경하는 로직만 추가하였고, 우측에는 리소스를 변경하는 과정에서 스레드를 일시 중단하는 코드를 추가하였다. 그 결과 입력처리 시간에 차이가 발생한다는 것을 확인할 수 있다. 41 | 42 | ![](https://cdn-images-1.medium.com/max/2912/1*Fq7VKzUg_c7WCcPy4ILJDw.png) 43 | 44 | 45 | 46 | 아래에는 이미지 뷰에 각각 다른 크기의 이미지 리소스를 배치하였다. 47 | 좌측에는 8.1MB 크기의 이미지 리소스를 배치하고 우측에는 16KB 크기의 이미지 리소스를 배치하였다. 그 결과 이미지를 업로드 시간에 차이가 발생한 다는 것을 확인할 수 있다. 48 | 49 | ![](https://cdn-images-1.medium.com/max/2932/1*_Cd42DYLhjYBcmPjOZroGw.png) 50 | 51 | 52 | 53 | 54 | 55 | ## GPU 오버드로우 시각화 56 | 57 | 다른 개발자 옵션으로 UI 에 컬러를 지정함으로써 오버드로우를 식별할 수 있다. 같은 프레임내에서 같은 픽셀을 한번 이상 그릴 때 오버드로우가 발생한다. 앱에서 필요 이상으로 많은 렌더링이 발생하는 곳을 시각화로 보여주며, 사용자에게 보여지지 않는 픽셀을 렌더링하기 위해 추가 GPU 작업으로 성능 문제가 발생한 수 있음을 알 수 있다. 다음과 같이 설정하면 오버드로우 시각화를 확인 할 수 있다. 58 | 59 | 1. 설정에서 개발자 옵션을 선택 60 | 2. 하드웨어 가속 렌더링 섹션에서 Debug GPU Overdraw 선택 61 | 3. 화면에 오버드로 영역 표시를 선택 62 | 63 | * **True color:** 오버드로우 없음 64 | 65 | * **Blue color:** 오버드로우 1회 66 | 67 | * **Green color:** 오버드로우 2회 68 | 69 | * **Pink color:** 오버드로우 3회 70 | 71 | * **Red color:** 오버드로우 4회 이상 72 | 73 | 뷰의 배치에 따라 각각 오버드로우가 발생한 픽셀을 확인할 수 있다. 74 | 75 | ![](https://cdn-images-1.medium.com/max/2000/1*FqRNnuyKanJHUKr7edRjwA.png) 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | [https://developer.android.com/topic/performance/rendering/inspect-gpu-rendering#debug_overdraw](https://developer.android.com/topic/performance/rendering/inspect-gpu-rendering#debug_overdraw) 84 | 85 | [https://developer.android.com/topic/performance/rendering/profile-gpu](https://developer.android.com/topic/performance/rendering/profile-gpu) 86 | 87 | 88 | -------------------------------------------------------------------------------- /003. View Rendering And Optimization/siyoungsong.md: -------------------------------------------------------------------------------- 1 | 2 | ## 안드로이드 View 어떻게 그려질까? 3 | > When an Activity receives focus, it will be requested to draw its layout. The Android framework will handle the procedure for drawing, but the Activity must provide the root node of its layout hierarchy. 4 | > Drawing begins with the root node of the layout. It is requested to measure and draw the layout tree. Drawing is handled by walking the tree and rendering each View that intersects the invalid region. In turn, each ViewGroup is responsible for requesting each of its children to be drawn (with the draw() method) and each View is responsible for drawing itself. Because the tree is traversed pre-order, this means that parents will be drawn before (i.e., behind) their children, with siblings drawn in the order they appear in the tree. 5 | > Drawing the layout is a two pass process: a measure pass and a layout pass. 6 | 7 | 액티비티가 포커스를 받을때, 액티비티는 레이아웃을 그리도록 요청을 한다. 안드로이드 프레임워크는 그리기 위한 절차를 다룬다. 액티비티는 레이아웃 계층의 루트 노드를 제공해야 한다. 8 | 9 | 그리기는 레이아웃의 루트 노드에서 시작한다. 레이아웃 트리를 측정하고 그려야 한다. 그리기는 트리를 따라서 유효하지 않는 영역과 교차하는 각각의 뷰를 렌더링한다. 차례로 각 뷰그룹은 차일드뷰에게 draw() 함수와 함께 그려질 것을 요청하며 각 뷰는 스스로 그리기를 담당한다. 트리는 이미 정해진 순서대로 순회하며, 이것은 부모뷰가 먼저 자식뷰보다 먼저 그려지며, 같은 계층의 뷰는 트리의 먼저 나타나는 것이 먼저 그려지는 것을 의미한다. 10 | 11 | 측정 단계와 레이아웃 두 단계 절차로 레이아웃을 그린다. 12 | 13 | ### Measure Pass 14 | 15 | 측정 패스는 measure(int, int) 로 구현되며 뷰 트리의 탑-다운으로 순회한다. 각 뷰는 트리를 재귀하는 동안 측정한 수치를 트리 아래로 보낸다. 측정이 모두 끝나면 모든 뷰는 측정한 값을 가지게 된다. 두번째 단계 또한 탑-다운으로 layout(int, int, int) 에서 일어난다. 각각의 부모는 측정단계에서 계산된 사이즈를 사용해서 자신의 모든 자식을 배치한다. 16 | 17 | ### Layout Pass 18 | 19 | 레이아웃을 시작하려면, requestLayout() 를 호출한다. 일반적으로 현재 범위내에 더이상 맞지 않을때 자체적으로 뷰에 의해 호출된다. 20 | 21 | 22 | 23 | ## 렌더링 24 | 25 | 앱에서 사용자의 품질 인식에 영향을 미칠수 있는 앱의 주요한 측면은 이미지와 텍스트가 스크린에 부드럽게 렌더링하는 것이다. 가장 중요한 것은 버벅거림과 늦은 응답을 피하는 것이다. 26 | 27 | ### Reduce overdraw 28 | 29 | 앱이 하나의 프레임안에서 같은 픽셀을 다시 그리는 횟수를 최소화한다. 30 | - 레이아웃에서 불필요한 배경 삭제 31 | - 뷰 계층 구조의 평면화 32 | - 투명도 줄이기 33 | 34 | ### Performance and view hierarchies 35 | 36 | 레이아웃 및 측정 실행을 효율적으로 하며, 중복적인 계산을 피한다. 37 | - 불필요한 중첩 레이아웃 제거 38 | - include / merge 적용 39 | 40 | ### Analyze with Profile GPU Rendering 41 | 42 | 프로파일 툴을 사용해서 렌더링이 느려질수 있는 병목 구간이 발생하는 부분을 확인한다. 43 | - 입력 처리 : 앱이 입력 이벤트를 처리하는 시간으로 입력 이벤트 콜백의 결과로 호출된 코드를 실행하는데 걸리는 시간을 나타낸다. 일반적으로 오랜 시간이 걸리는 경우는 메인쓰레드에서 발생하며, 작업을 최적화하거나 다른 쓰레드를 이용해서 작업을 실행하도록 한다 44 | - 애니메이션 : 현재 프레임에서 실행되는 애니메이션에 대해 걸리는 시간을 측정한다. 일반적으로 오랜 시간이 걸리는 경우는 애니메이션의 속성 변경으로 인해 발생한다. 45 | - 측정 및 배치 : 안드로이드에서 뷰를 스크린에 그리기 위해 측정하고 배치하는데 걸리는 시간을 측정한다. 일반적으로 오랜 시간이 걸리는 경우는 배치할 뷰가 너무 많거나 혹은 계층구조의 잘못된 장소에서 중복 계산이 이뤄지며, Traceview와 Systrace 를 사용해서 호출 스택을 확인하여 문제를 파악할 수 있다. 46 | - 그리기 : 백그라운드 혹은 텍스트 그리기와 같은 뷰를 그리기 명령어로 번역하는 단계로 시스템은 명령어를 디스플레이 목록에 캡처한다. 무효화된 뷰에서 onDraw 호출을 실행해 걸리는 모든 시간을 측정하며, 오랜 시간이 걸린다는 것은 무효화된 뷰가 많다는 것을 의미한다. 뷰가 무효화되는 경우 뷰의 디스프레이 목록을 재생성한다. 혹은 커스텀한 뷰가 onDraw 할때 복잡한 로직을 가질 때도 실행시간이 오랜 걸린다. 47 | - 동기화/업로드 : 현재 프레임에서 비트맵 객체가 CPU 메모리에서 GPU 메모리로 전송되는데 걸리는 시간을 측정한다. 오랜 시간이 걸리는 경우는 작은 리소스가 많이 로드되어 있거나 적지만 큰 리소스가 로드되어 있는 경우이다. 비트맵 해상도가 실제 디스플레이 해상도보다 큰 사이즈로 로드되어 있지 않도록 하거나 동기화 전에 비동기로 미리 업로드해서 실행 시간을 줄일 수 있다. 48 | - 명령어 실행 : 디스플레이 목록을 화면에 그리기 위한 모든 명령어를 실행하기 위해 걸리는 시간을 측정한다. 주어진 프레임에서 렌더링하는 디스플레이 목록과 수량을 직접적으로 측정한다. 내재적으로 많은 그리기 동작이 있는 경우, 오랜 시간이 걸릴수 있다. 49 | - 프로세스/스왑버퍼 : 안드로이드는 디스플레이 목록을 GPU 에 제출하는것을 끝내면, 시스템은 그래픽 드라이버에 현재 프레임이 완료되었음을 알리며 드라이버를 업데이트된 이미지를 화면에 표시한다. CPU 와 GPU 는 병렬로 처리되는데 CPU 가 GPU 처리보다 빠른 경우, 프로세스간 통신 큐가 가득찰 수 있다. CPU 는 큐에 공간이 생길때까지 기다린다. 큐가 가득한 상태는 스왑버퍼 상태에서 자주 발생하는데 이 단계에서 프레임 전체의 명령어가 제출되기 때문이다. 명령어 실행 단계와 비슷하게 GPU 에서 일어나는 일의 복잡성을 줄여 문제를 해결할 수 있다. 50 | - 기타 : 렌더링 시스템이 작업을 수행하는 시간외에 렌더링과 관련 없는 추가적인 작업이 있다. 일반적으로 렌더링의 연속된 두 프레임 사이에서 UI 스레드에서 발생할 수 있는 일을 나타낸다. 오랜 시간이 걸리는 경우, 다른스레드에서 실행해야할 콜백, 인텐트 또는 기타 다른 작업이 있을 수 있다. Method tracing 또는 Systrace 를 사용하면 메인스레드에서 실행중인 작업을 확인해 문제를 해결할 수 있다. 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | [https://developer.android.com/guide/topics/ui/how-android-draws](https://developer.android.com/guide/topics/ui/how-android-draws) 60 | 61 | [https://developer.android.com/topic/performance/rendering](https://developer.android.com/topic/performance/rendering) 62 | 63 | [https://developer.android.com/topic/performance/rendering/inspect-gpu-rendering](https://developer.android.com/topic/performance/rendering/inspect-gpu-rendering) 64 | -------------------------------------------------------------------------------- /004. Image Loading and Caching Library/004_Image Loading and Caching Library_Part3.md: -------------------------------------------------------------------------------- 1 | # Image Loading and Caching Library Part 3 - Performance Test 2 | 3 | 이미지 라이브러리들 중 Glide, Fresco, Coil에 대해서 성능 테스트를 해보자. 4 | 5 | 성능 테스트에 사용한 기기는 `Pixel 4a`, 값의 측정은 `Android Studio Profiler`를 사용하였다. 6 | 7 | 각 세트는 동일한 조건에서 테스트하였으며, 별도의 추가 옵션없이 각 라이브러리의 기본 세팅으로 테스트를 수행하였다. 8 | 9 | 측정값은 이미지 로딩 전후로 CPU, Memory, Disk를 10번씩 측정한 후의 평균값이다. 10 | 11 | 테스트 애플리케이션의 스토리지가 증가한 뒤, 스토리지를 비우고 다시 측정하였다. 12 | 13 | > **참고** 기본 세팅이니만큼 Picasso의 경우 거대한 이미지를 대부분 불러오지 못했다. 14 | 15 | ### Case 1) 하나의 거대한 이미지를 출력하는 경우 16 | 17 | 테스트에 쓰인 이미지는 `7,680 x 4,320`의 사이즈와 `4.29mb`의 용량을 가진 24-bit의 color depth를 가진 이미지이다. 18 | 19 | ![one big image profiling](https://i.imgur.com/m7itaqy.png) 20 | 21 | | Library | CPU | Memory | Disk | 22 | |:--:|:--:|:--:|:--:| 23 | |Coil|38.3%|4.26MB|X| 24 | |Glide|40.8%|3.41MB|0.21MB| 25 | |Fresco|40.2%|8.14MB|X| 26 | 27 | 1개의 거대한 이미지의 경우 특기할만한 성능 차이는 존재하지 않았다. 28 | 29 | ### Case 2) 동일한 이미지를 여러 번 출력하는 경우 30 | 31 | Case 1의 이미지를 ScrollView에 50개 추가하여 테스트하였다. 32 | 33 | ![50 big image profiling](https://i.imgur.com/BntHJ2Y.png) 34 | 35 | | Library | CPU | Memory | Disk | 36 | |:--:|:--:|:--:|:--:| 37 | |Coil|93.6%|350.8MB|X| 38 | |Glide|45.4%|4.6MB|0.21MB| 39 | |Fresco|72.4%|8.4MB|X| 40 | 41 | Coil의 Cpu, Memory 사용량이 매우 크게 증가하였다. 42 | 43 | Glide는 앱을 끈 후 다시 로딩했을 때 딜레이가 없었어 재로딩 했을 때 매우 효율적으로 동작하였다. 44 | 45 | 아래 세부 프로파일링을 보면 Coil이 Coroutine을 사용할 때 CPU, Memory가 정말 많이 사용된다는 것을 알 수 있다. 46 | 47 | 아래는 Coil로 테스트하였을 때의 프로파일링 결과이다. 48 | 49 | ![50 big image profiling coil](https://i.imgur.com/rb1bMMt.png) 50 | 51 | 아래는 Glide로 테스트하였을 때의 프로파일링 결과이다. 52 | 53 | ![50 big image profiling glide](https://i.imgur.com/0oDUU69.png) 54 | 55 | Glide가 하나의 이미지를 여러개 불러올 때 제일 효율이 좋은 것으로 나타났다. 56 | 57 | ### Case 3) 여러 이미지를 각각 출력해보는 경우 (feat_ ScrollView) 58 | 59 | 2mb ~ 10mb 사이의 각기 다른 용량을 가진 이미지 10개를 ScrollView에 추가하여 테스트하였다. 60 | 61 | ![10 different image profiling](https://i.imgur.com/TIrybR8.png) 62 | 63 | | Library | CPU | Memory | Disk | 64 | |:--:|:--:|:--:|:--:| 65 | |Coil|88.6%|800MB|X| 66 | |Glide|89%|303.3MB|4.6MB| 67 | |Fresco|92.6%|276MB|X| 68 | 69 | 캐싱을 기대할 수 없는 만큼 대체적으로 모두 CPU, Memory사용량이 올라갔다. 70 | 71 | 그 중 Coil이 제일 많은 Memory를 사용하였다. 72 | 73 | ### Case 4) 여러 이미지를 각각 출력해보는 경우 (feat_ RecyclerView) 74 | 75 | Case 3에사 사용한 이미지를 RecyclerView에 추가하여 테스트하였다. 76 | 77 | ![10 different image recyclerView profiling](https://i.imgur.com/QafKUB1.png) 78 | 79 | | Library | CPU | Memory | Disk | 80 | |:--:|:--:|:--:|:--:| 81 | |Coil|94%|800MB|X| 82 | |Glide|94%|270MB|4.52MB| 83 | |Fresco|94.6%|281MB|X| 84 | 85 | Coil의 경우 ScrollView에 로딩할 때보다 CPU 사용량이 증가한 것을 확인할 수 있었다. 86 | 87 | ### 마무리 88 | 89 | Coil이 Coroutine으로 만들어져 기대를 많이 하고 테스트를 해보았는데 생각보다 CPU, Memory의 사용량이 많았다. 90 | 91 | Gilde는 정말로 모든 부분에서 최적화가 잘 되어있는 것으로 보이는 것에 비해, Picasso는 잘 사용하고 싶다면 정말 커스텀을 많이, 잘 해야될 필요가 있어보인다. 92 | 93 | Fresco는 최적화가 정말 잘 되어있지만 러닝커브가 높다. 퍼포먼스 측면에서는 Glide와 제일 비슷하였다. 94 | 95 | ## References 96 | 97 | ### Members of Study 98 | - https://namhoon.kim/2021/05/25/android_deep_dive/017_Image%20Loading%20and%20Caching%20Library%20Part%201/ 99 | - https://namhoon.kim/2021/06/01/android_deep_dive/018_Image%20Loading%20and%20Caching%20Library%20Part%202/ 100 | - https://velog.io/@jshme/Android-Image-Loader-Library 101 | - https://velog.io/@jshme/Android-Hello-Out-Of-Memory 102 | - https://www.charlezz.com/wordpress/wp-content/uploads/2020/10/www.charlezz.com-glide-v4-glide-v4--by-charlezz.pdf 103 | 104 | ### Official 105 | 106 | - [Android Developers#Drawble Resources](https://developer.android.com/guide/topics/resources/drawable-resource.html) 107 | - [Android Developers#Handling bitmaps](https://developer.android.com/topic/performance/graphics/index.html) 108 | - [Android Developers#LruCache](https://developer.android.com/reference/android/util/LruCache) 109 | - [androidx.collection.LruCache](https://developer.android.com/reference/androidx/collection/LruCache) 110 | - [Cache replacement policies#recently used](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)) 111 | - [Google#Accompanist Repository](https://github.com/google/accompanist) 112 | 113 | - https://square.github.io/picasso/ 114 | - https://bumptech.github.io/glide/ 115 | - https://github.com/coil-kt/coil 116 | - https://frescolib.org 117 | - https://github.com/nostra13/Android-Universal-Image-Loader/wiki 118 | 119 | ### ETC 120 | 121 | - https://futurestud.io/tutorials/glide-caching-basics 122 | - https://wooooooak.github.io/%EB%B2%88%EC%97%AD%ED%95%98%EB%A9%B0%20%EA%B3%B5%EB%B6%80%ED%95%98%EA%B8%B0/2021/02/21/How-The-Android-Image-Loading-Library-Glide-and-Fresco-Works/ 123 | 124 | --- 125 | 126 | 해당 포스트는 아래 팀원들과 함께 작성되었습니다. 127 | 128 | - 곽욱현 @Knowre 129 | - 김남훈 @Naver 130 | - 배희성 @Rocketpunch 131 | - 송시영 @Smartstudy 132 | - 옥수환 @Naver 133 | - 이기정 @Banksalad 134 | - 정세희 @Banksalad 135 | 136 | 함께 공부하고 싶으신 분들은 [여기](https://github.com/AndroidDeepDive/Contact-Us/issues)에 이슈를 등록해주세요! 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /004. Image Loading and Caching Library/heesungbae.md: -------------------------------------------------------------------------------- 1 | ## **성능 테스트** 2 | 3 | 테스트할 라이브러리 종류는 총 3가지(Coil, Glide, Fresco)다. 4 | 디바이스는 `Pixel 4a`, 값 체크는 `Android Studio Profiler`를 사용하였다. 5 | 6 | ``` 7 | 각 테스트는 동일한 조건에서 테스트 하였고 추가 옵션 없이 기본 세팅으로 테스트하였다. 8 | 이미지 로딩 전 후로 CPU, Memory, Disk를 10번 체크 후 평균에 대한 값을 확인하였다. 9 | 단독 실행으로 진행되었으며 스토리지가 증가한 이후에는 스토리지를 비우고 다시 확인하였다. 10 | ``` 11 | 12 | ``` 13 | Picasso도 테스트 하였지만 대부분의 큰 이미지를 잘 불러오지 못해 제외하였다. 14 | ``` 15 | 16 | ### **거대한 이미지 하나를 출력해보는 경우** 17 | --- 18 | 19 | 이미지는 7,680 x 4,320(24-bit color) 4.29MB 이미지를 사용하였다. 20 | 21 | ![one big image profiling](https://i.imgur.com/m7itaqy.png) 22 | 23 | |이름| CPU | Memory | Disk | 24 | |---|---|---|---| 25 | |Coil|38.3%|4.26MB|X| 26 | |Glide|40.8%|3.41MB|0.21MB| 27 | |Fresco|40.2%|8.14MB|X| 28 | 29 | ``` 30 | 1개의 커다란 이미지를 로딩할 때에는 큰 성능 차이는 찾지 못했다. 31 | ``` 32 | 33 | 34 | ### **동일한 이미지를 여러 번 출력해보는 경우 (Chacing)** 35 | --- 36 | 확실한 성능 차이를 확인하기 위해, 37 | 위에서 사용한 이미지 50개 ScrollView에 추가해서 확인해보았다. 38 | 39 | ![50 big image profiling](https://i.imgur.com/BntHJ2Y.png) 40 | 41 | |이름| CPU | Memory | Disk | 42 | |---|---|---|---| 43 | |Coil|93.6%|350.8MB|X| 44 | |Glide|45.4%|4.6MB|0.21MB| 45 | |Fresco|72.4%|8.4MB|X| 46 | 47 | Coil의 Cpu, Memory 사용량이 매우 크게 증가하였다. 48 | 49 | Glide는 앱을 끈 후 다시 로딩했을 때 딜레이가 없었어 재로딩 했을 때 매우 효율적으로 동작하였다. 50 | 51 | 아래 세부 프로파일링을 보면 Coil이 Coroutine을 사용할 때 CPU, Memory가 정말 많이 사용된다는 것을 알 수 있다. 52 | 53 | ![50 big image profiling coil](https://i.imgur.com/rb1bMMt.png) 54 | ``` 55 | Coil 세부 프로파일링 56 | ``` 57 | ![50 big image profiling glide](https://i.imgur.com/0oDUU69.png) 58 | ``` 59 | Glide 세부 프로파일링 60 | ``` 61 | 62 | ``` 63 | Glide가 하나의 이미지를 여러개 불러올 때 제일 효율이 좋아보였다. 64 | ``` 65 | 66 | ### **여러 이미지를 각각 출력해보는 경우** 67 | --- 68 | 동일한 이미지가 아니라 각각 다른 이미지 여러개를 출력할 때에는 어떻게 다른지 확인해보았다. 69 | 2~10MB의 큰 이미지 10개를 ScrollView에 추가해서 확인해보았다. 70 | 71 | ![10 different image profiling](https://i.imgur.com/TIrybR8.png) 72 | 73 | |이름| CPU | Memory | Disk | 74 | |---|---|---|---| 75 | |Coil|88.6%|800MB|X| 76 | |Glide|89%|303.3MB|4.6MB| 77 | |Fresco|92.6%|276MB|X| 78 | 79 | 80 | ``` 81 | 대체적으로 모두 CPU, Memory사용량이 올라갔다. 82 | 그 중 Coil이 제일 많은 Memory를 사용하였다. 83 | ``` 84 | 85 | ### **RecyclerView 기반으로 출력해보는 경우 (Chacing / Disk)** 86 | --- 87 | 위에서 사용한 이미지(2~10MB의 큰 이미지 10개)들을 ScrollView가 아닌 RecyclerView에 추가해서 확인해보았다. 88 | 89 | ![10 different image recyclerView profiling](https://i.imgur.com/QafKUB1.png) 90 | 91 | |이름| CPU | Memory | Disk | 92 | |---|---|---|---| 93 | |Coil|94%|800MB|X| 94 | |Glide|94%|270MB|4.52MB| 95 | |Fresco|94.6%|281MB|X| 96 | 97 | ``` 98 | Coil의 경우 ScrollView에 로딩할 때보다 CPU사용량이 올라갔다. 99 | ``` 100 | 101 | 102 | ### 성능 테스트 소감 103 | --- 104 | Coil이 Coroutine으로 만들어져 기대를 많이 하고 테스트를 해보았는데 생각보다 CPU, Memory의 사용량이 많았다. 105 | 106 | Gilde는 정말로 모든 부분에서 최적화가 잘 되어있다고 생각했다. 107 | 108 | Picasso는 잘 사용하고 싶다면 정말 커스텀을 많이, 잘 해야될 필요가 있어보인다. 109 | 110 | Fresco는 최적화가 정말 잘 되어있지만 러닝커브가 높다. 퍼포먼스 측면에서는 Glide와 제일 비슷하였다. 111 | -------------------------------------------------------------------------------- /004. Image Loading and Caching Library/siyoung.md: -------------------------------------------------------------------------------- 1 | 2 | ## 어떤 이미지 라이브러리 사용할까? 3 | 4 | ## Imageview 와 bitmap 5 | 6 | 안드로이드에서는 bitmap 또는 drawable 와 같은 리소스를 가지고 이미지를 화면에 표시할 수 있다. bitmap 을 로딩해 이미지를 표시하는 할 때 주의해서 사용을 해야한다. 7 | 8 | * bitmap 은 쉽게 앱의 메모리한도를 고갈시킬 수 있다. 예를 들어 픽셀 폰은 카메라 사진이 4048x3036 픽셀(12 메가픽셀)까지 찍을수 있다. bitmap 구성이 ARGB_8888 인 경우, 기본적으로 안드로이드 2.3 (API level 9) 이상에서는 하나의 사진을 메모리에 로딩하기 위해 48MB 의 메모리를 차지하게 된다. 이렇게 큰 메모리를 요구하면 앱에서 사용할 수 있는 모든 메모리를 즉시 사용하게 될 수 있다. 9 | 10 | * UI 스레드에서 비트맵을 로딩하는 것은 앱의 성능을 저하되어 늦은 응답성 또는 ANR 메시지와 같은 원인이 된다. 따라서 bitmap 을 작업할 때에는 스레드를 적절하게 관리하는 것이 중요하다. 11 | 12 | * 앱에서 여러 bitmap 을 메모리에 로딩할 때에는, 메모리 관리와 디스크 캐싱이 필요하다. 그렇지 않으면, 앱의 응답성과 유동성이 나빠질 수 있다. 13 | 14 | 앱에서 bitmap 을 가져와서 디코딩하고 표시하기 위해서는 이미지 라이브러를 사용하는 것이 좋다. 이미지 라이브러리는 bitmap 과 관련된 다양하고 복잡한 과정을 대신 관리해주며, 손쉽게 사용할 수 있도록 되어있다. 15 | 16 | 17 | 18 | ## 안드로이드 이미지 라이브러리 19 | 20 | ### Picasso 21 | 22 | 이미지는 컨텍스트와 시각적 감각을 안드로이드 어플리케이션에 추가한다. Picasso 는 번거로움 없이 종종 단 한줄의 코드만으로도 이미지를 로딩할 수 있게 해준다. 어댑터에서 이미지뷰를 재사용하고 다운로드와 취소할 수 있다. 적은 메모리 사용으로 복잡한 이미지 변환을 할 수 있다. 자동 메모리와 디스크 캐싱을 지원한다. 23 | 24 | ### Glide 25 | 26 | 글라이드는 빠르고 효율적으로 미디어 관리 오픈소스이다. 미디어 디코딩, 메모리 및 디스크 캐싱 그리고 리소스 풀링을 간단하고 사용하기 쉽게 인터페이스로 래핑된 안드로이드 이미지 로딩 프레임워크이다. 페치, 디코딩, 그리고 비디오스틸, 이미지, 움직이는 GIF 를 표시할 수 있다. 개발자가 거의 모든 네트워크 스택에 연결할 수 있는 유연한 API 를 포함한다. 커스텀된 HttpUrlConnection 을 기본 스택으로 사용하지만, 구글 볼리 프로젝트 또는 스쿼어의 OkHttp 라이브러리를 대신 사용할 수 있다. 기본적으로 어떠한 이미지 목록도 부드럽고 빠르게 스크롤링 하는것이 초점이 맞춰져 있지만, 또한 거의 모든 경우 어디서라도 페치, 리사이즈, 그리고 원격 이미지를 표시가 필요할 때에도 효과적이다. 27 | 28 | * 메모리 캐시는 기본적으로 LruResourceCache 방식을 사용하여 메모리 캐시가 구현되어 있으며 디바이스의 메모리 사이즈나 스크린 해상도에 관계 없이 Glide 의 MemorySizeCalculator 에 의해 캐시 사이즈가 계산된다. 29 | 30 | * 디스크 캐시 또한 Lru 기반으로 동작하며, 기본적으로 250MB 의 디스크캐시를 가지며, 어플리케이션 폴더내에 특정 디렉토리에 있다. 31 | 32 | * Lru 기반의 Bitmap Pool 을 사용하여 스크린 사이즈와 밀도를 기반으로 MemorySizeCalculator 에 의해 Bitmap Pool 사이즈가 정해진다. 33 | 34 | * Glide 는 RGB_565 Bitmap 포맷을 기본으로 동작하며, 화질은 다소 떨어지나 메모리 효율은 좀 더 낫다. 35 | 36 | * gif, jpeg, war, png, webp 포맷을 지원한다. 37 | 38 | ### Fresco 39 | 40 | 프레스코는 안드로이드 어플리케이션에서 이미지를 표시하기 위한 강력한 시스템이다. 이미지 로딩과 표시를 관리한다. 네트워크, 로컬 스토리지 또는 로컬 리소스로부터 이미지를 로드하고 이미지가 로드되기전까지 플레이스홀더를 표시한다. 메모리와 내부 스토리지 두 단계의 캐시를 가지고 있다. 안드로이드 4.0 이하에서, 이미지를 안드로이드 메모리의 특별한 영역에 두어 어플리케이션이 빠르고 OutOfMemoryError 가 적게 발생하도록 한다. 41 | 42 | ### Coil 43 | 44 | 코일은 Coroutine Image Loader 의 약어로서 코틀린 코루틴 기반 만들어진 안드로이드 이미지 라이브러리이다. 메모리, 디스크 캐싱, 메모리에서 이미지의 다운샘플링, 비트맵 재사용, 자동 일시중지 및 취소 등의 많은 최적화 수행으로 빠르다. (앱에 이미 OkHttp 와 코루틴이 포함된 경우) 2000 개의 메소드만을 추가한다. 이는 피카소와 비슷한 수의 메소드를 가지거나 글라이드, 프레스코 현저히 적은 메소드를 가지며 가볍다는 것을 의미한다. API 는 코틀린 언어 기능을 활용하여 단순하고 적은 보일러 플레이트로 사용하기 쉽다. 코틀린 우선으로 코루틴, OkHttp, Okio, AndroidX Lifecycles 등의 모던한 라이브러리를 포함하고 있다. 45 | 46 | * Dynamic image sampling 을 지원한다. 500x500 이미지를 100x100 이미지뷰에 로드할 때, Coil 은 100x100 이미지로 미리 로딩하여 우선 placeholder 로 사용한다. 이후에 500x500 이미지가 로딩되어 사용이 가능해지면 500x500 으로 바꾸어 이미지 로딩을 한다. 47 | 48 | * 메모리 캐시는 MemoryCache.Key 를 사용하여 로드된 이미지를 메모리 캐시에 저장할 수 있다. 디바이스가 가진 메모리 크기에 따라서 0.15~0.20% 메모리를 디스크 캐시로 사용한다. 49 | 50 | * 디스크 캐시는 OkHttpClient 을 기반으로 디스크 캐시를 처리하며 디바이스 남아있는 공간에 따라 최대 10–250MB 까지 정해진다. 커스텀 OkHttpClient 을 설정해 사용하는 경우, CoilUtils.createDefaultCache 을 옵션으로 사용할 수 있으며 다른 크기와 위치를 가지는 캐시 인스턴스를 만들수 있다. 51 | 52 | * bmp, jpeg, png, webp, heif (Android 8.0+) 포맷을 지원한다. 53 | 54 | * coil-gif 를 추가하면 GIF, animated WebP (Android 9.0+), animated HEIF (Android 11.0+) 을 추가로 사용할 수 있다. 55 | 56 | * coil-svg 를 디펜던시로 추가하면 svg 를 사용할 수 있다. 57 | 58 | * coil-video 를 디펜던시로 추가하면 안드로이드가 지원하는 모든 비디오 코덱으로 부터 정적 비디오 프레임을 사용할 수 있다. 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | [https://developer.android.com/topic/performance/graphics](https://developer.android.com/topic/performance/graphics) 69 | 70 | [https://square.github.io/picasso/](https://square.github.io/picasso/) 71 | [**bumptech/glide** 72 | *View Glide's documentation | 简体中文文档 | Report an issue with Glide Glide is a fast and efficient open source media…*github.com](https://github.com/bumptech/glide) 73 | [**Glide - Caching Basics** 74 | *After we've looked at loading, displaying and manipulating images, we'll move on to optimize the process. One of the…*futurestud.io](https://futurestud.io/tutorials/glide-caching-basics) 75 | [**How The Android Image Loading Library Glide and Fresco Works?(번역)** 76 | *원본 - How The Android Image Loading Library Glide and Fresco Works? 저는 오늘 어렵게 슥듭한 지식을 공유해보려합니다. 지식은 그것을 갈망하는 자에게 온다. 따라서…*wooooooak.github.io](https://wooooooak.github.io/%EB%B2%88%EC%97%AD%ED%95%98%EB%A9%B0%20%EA%B3%B5%EB%B6%80%ED%95%98%EA%B8%B0/2021/02/21/How-The-Android-Image-Loading-Library-Glide-and-Fresco-Works/) 77 | 78 | [https://www.charlezz.com/wordpress/wp-content/uploads/2020/10/www.charlezz.com-glide-v4-glide-v4--by-charlezz.pdf](https://www.charlezz.com/wordpress/wp-content/uploads/2020/10/www.charlezz.com-glide-v4-glide-v4--by-charlezz.pdf) 79 | -------------------------------------------------------------------------------- /004. Image Loading and Caching Library/siyoung_appendix.md: -------------------------------------------------------------------------------- 1 | 2 | ## Jetpack Compose 이미지 라이브러리 사용하기 3 | 4 | ## **Accompanist** 5 | 6 | 개발자가 일반적으로 필요로 하지만 아직 사용할 수 없는 Jetpack Compose 의 기능을 지원하는 라이브러리 그룹이다. Coil, Glide 라이브러를 사용하게 도와 줄뿐 아니라 여러 레이아웃이나 레이아웃을 설정을 할 수 있게 도와준다. 7 | 8 | accompanist 에서 Glide 와 Coil 을 사용하기 위해서는 아래와 같이 dependency 를 추가한다. 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | dependencies { 15 | implementation "com.google.accompanist:accompanist-glide:0.11.1" 16 | implementation "com.google.accompanist:accompanist-coil:0.11.1" 17 | // Coil 에서 gif 를 사용하기 위해서 추가 18 | implementation "io.coil-kt:coil-gif:1.2.2" 19 | } 20 | 21 | 22 | 아래와 같이 Glide 와 Coil 를 사용하기 위한 코드를 작성할 수 있다. 23 | 24 | *// Glide* 25 | *Image*( 26 | painter = *rememberGlidePainter*( 27 | request = "https://picsum.photos/300/300", 28 | ), 29 | contentDescription = null 30 | *) 31 | Image*( 32 | painter = *rememberGlidePainter*( 33 | request = "https://cataas.com/cat/gif", 34 | ), 35 | contentDescription = null 36 | *)* 37 | 38 | *// Coil 39 | Image*( 40 | painter = *rememberCoilPainter*( 41 | request = "https://picsum.photos/300/300", 42 | ), 43 | contentDescription = null 44 | *) 45 | Image*( 46 | painter = *rememberCoilPainter*( 47 | request = "https://cataas.com/cat/gif", 48 | imageLoader = *gifImageLoader*(*LocalContext*.current), 49 | ), 50 | contentDescription = null 51 | ) 52 | 53 | rememberGlidePainter / rememberCoilPainter 는 각각 Glide.kt / Coil.kt 에 Glide 와 Coil 라이브러리를 Composable function 으로 래핑되어 기존의 라이브러리를 Compose 에서도 손쉽게 사용할 수 있도록 되어 있다. 54 | 55 | Coil 에서 gif 를 사용하기 위해서는 아래와 같이 별로의 이미지 로더 설정이 필요하다. 56 | 57 | fun gifImageLoader(context: Context) = ImageLoader.Builder(context) 58 | .componentRegistry {** 59 | **if (*SDK_INT *>= 28) { 60 | add(ImageDecoderDecoder(context)) 61 | } else { 62 | add(GifDecoder()) 63 | } 64 | }** 65 | **.build() 66 | 67 | 68 | 69 | 원격으로 이미지를 호출하는 경우, manifest 에 퍼미션을 추가한다. 70 | 71 | 72 | 73 | 간혹 퍼미션을 추가했음에도 데이터를 로드하지 못하는 경우가 발생하면, 앱을 삭제했다가 다시 설치하면 정상적으로 로드할 수 있다. 74 | 75 | 76 | 77 | Accompanist 에서 더 자세한 Glide / Coil 사용법과 기타 다른 라이브러리는 아래 사이트에서 확인할 수 있다. 78 | 79 | [https://google.github.io/accompanist/](https://google.github.io/accompanist/) 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /004. Image Loading and Caching Library/toc.md: -------------------------------------------------------------------------------- 1 | ## Image Loading and Caching Library TOC 2 | 3 | ### Part 1 - 이미지 라이브러리를 사용해야하는 이유 4 | - ImageView / Bitmap 5 | - Widget 및 Component 설명 6 | - Andorid api만으로 이미지 렌더링하기 7 | - 고려해야하는 점 8 | - Out of memory 9 | - Slow Loading 10 | - Bitmap Caching 11 | - Memory Cache 12 | - LRU Algorithm 13 | - Unresponse UI 14 | - Gabarge collector issue 15 | - SoftReference / WeakReference 16 | - Image Library 소개 17 | - Picasso 18 | - Glide 19 | - Coil 20 | - Fresco 21 | - AMUL 22 | - Volley 23 | 24 | ### Part 2 - Image Library별 특징과 취사 선택 25 | - Picasso 26 | - Glide 27 | - Coil 28 | - Jetpack Compose 29 | - kotlin base 30 | - Fresco 31 | - WebP 32 | - 어떤 케이스에 어떤 라이브러리가 적합한가? 33 | 34 | ### Part 3 - 성능 테스트 35 | - 1. 거대한 이미지 하나를 출력해보는 경우 36 | - 2. 동일한 이미지를 여러 번 출력해보는 경우 (Chacing) 37 | - 3. 여러 이미지를 각각 출력해보는 경우 38 | - 4. RecyclerView 기반으로 출력해보는 경우 (Chacing / Disk) 39 | - 5. Trasform 40 | 41 | ### Part 4 - 마무리, 대표적인 서비스들은 어떤 이미지 라이브러리를 쓰고 있을까? 42 | -------------------------------------------------------------------------------- /005. Kotlin Symbol Processing API/All about KSP - Part2 KSP 살펴보기.md: -------------------------------------------------------------------------------- 1 | # All about KSP - Part2: KSP 살펴보기 2 | 3 | ## KSP란 무엇인가? 4 | 5 | **KSP(Kotlin Symbol Processing)**은 2021년 2월 10일 구글이 발표한 코틀린에서 경량화 된 컴파일러 플러그인을 개발할 수 있는 API다. 학습곡선을 최소한으로 줄이고, 코틀린의 기능을 활용할 수 있는 단순화된 API를 제공한다. 여러 플랫폼에 호환성을 염두하고 만들어졌으며, 코틀린 1.4.30 버전 이상부터 호환된다. 6 | 7 | KSP는 코틀린 언어가 갖는 특징인 확장 함수, 로컬 함수 같은 기능을 이해한다. 또한 KSP는 타입을 명시적으로 다루고, 기본적인 타입 검사와 동등성 검사를 지원한다. 8 | 9 | API는 코틀린 문법에 따라 symbol 수준에서 코틀린 프로그램 구조를 모델링 한다. KSP 기반 플러그인이 소스 프로그램을 처리할 때 클래스, 클래스 멤버, 함수 및 관련 매개 변수와 같은 구성은 프로세서에서 쉽게 접근할 수 있기 때문에 코틀린 개발자들에게 편리하다. 개념적으로 KSP는 Kotlin 리플렉션의 KType과 유사하다. API 를 사용하면 프로세서가 클래스 선언에서 특정 타입 인자가 있는 해당 타입, 또는 그 반대로 탐색할 수 있다. 10 | 11 | KAPT와 비교했을 때 KSP를 사용하는 [애노테이션 프로세서](https://www.charlezz.com/?p=1167)는 최대 **2배** 더 빠르게 실행할 수 있다. 자세한 내용은 [KSP Github 리포지토리](https://github.com/google/ksp)에서 오픈 소스 코드 및 문서를 확인할 수 있다. 12 | 13 | ## KSP를 왜 사용해야 할까? 14 | 15 | 컴파일러 플러그인은 코드 작성 방법을 크게 향상시킬 수 있는 강력한 메타프로그래밍 도구이다. 컴파일러 플러그인은 컴파일러를 라이브러리로 직접 호출하여 입력 프로그램을 분석하고 편집한다. 이러한 플러그인은 다양한 용도로 쓰일 수 있는 산출물을 생성한다. 예를 들어, bolierplate 코드를 생성할 수 있고 Parcelble과 같은 특별히 마크된 프로그램 요소에 대한 전체 구현을 생성할 수도 있다. 플러그인은 다양한 용도로 사용되며 언어로 직접 제공되지 않는 기능을 구현하고 미세 조정하는 데 사용할 수도 있다. 16 | 17 | 컴파일러 플러그인은 강력하지만, 역시 대가는 따른다. 가장 간단한 플러그인을 작성하려고만 해도, 컴파일러 배경 지식이 있어야 하며 특정 컴파일러의 구현 세부 사항에 대해 어느 정도 익숙해져야 한다. 또 다른 문제는 플러그인이 특정 컴파일러 버젼과 밀접하게 연관되어 있다는 점이다. 즉 최신 버젼의 컴파일러를 지원할 때 마다 플러그인을 업데이트 해야 할 수도 있다. 18 | 19 | KSP는 컴파일러 변경 사항을 숨기도록 설게되어 KSP를 사용하는 Processor의 유지 보수가 최소화 된다. KSP는 또한 JVM에 종속되지 않도록 설계되어 향후 다른 플랫폼에 쉽게 적응할 수 있기도 하다. KSP는 빌드 시간을 최소화 시키도록 설계되었다. Glide 와 같은 일부 Processor의 경우 KSP는 KAPT와 비교할 때 컴파일 시간을 25%까지 줄인다. 20 | 21 | #### `kotlinc` 컴파일러 플러그인과의 비교 22 | 23 | `kotlinc` 컴파일러 플러그인의 경우 강력한 기능을 제공하는 것은 맞지만, 그만큼 컴파일러에 대한 의존성이 크기때문에 유지보수에 대한 용이성이 떨어진다. 반면 KSP는 대부분의 컴파일러 변경사항을 은닉하여 api를 통해 접근할 수 있도록 해준다. 물론 한 단계를 더 건너야하는 만큼 `kotlinc`의 모든 기능을 지원하지는 않지만, 기술 부채를 고려하였을 때 합리적인 선택이 될 것이다. 24 | 25 | #### `kotlin.reflect`와의 비교 26 | 27 | KSP는 `kotlin.reflect`와 유사하게 생겼지만, KSP는 타입에 대한 참조를 명시적으로 지정해주어야 한다. 28 | 29 | #### KAPT와의 비교 30 | 31 | 앞선 포스팅에서 언급했듯 KAPT는 Kotlin 코드를 Java Annotation Processor를 수정하지 않기 위해 컴파일시 Java로 된 Stub을 생성하게 된다. Stub을 생성하는 것은 kotlinc의 분석 비용의 3분의 1이나 차지하므로, 빌드시 많은 오버헤드가 발생하게 된다. Glide를 기준으로 KSP로 전환시 컴파일타임이 25% 감소했다고 한다. KAPT와 달리 KSP는 Java 관점이 아닌 Kotlin의 관점에서 접근하며, `top-level function`과 같은 Kotlin의 고유 기능에 더 적합하다. 32 | 33 | ## KSP의 한계점 34 | 35 | KSP는 일반적인 유즈케이스에 대한 간단한 솔루션이 되고자 한다. 다른 플러그인 솔루션에 비해 몇가지 절충점(trade-off)이 있다. 36 | 37 | **다음은 KSP의 목표가 아니다.** 38 | 39 | 1. 소스 코드의 표현 수준 정보를 조사하기 40 | 2. 소스 코드 수정하기 41 | 3. Java Annotation Processing API와 100% 호환하기 42 | 4. IDE와 통합하기 (현재는 IDE가 생성 된 코드를 읽지 못함) 43 | 44 | 안드로이드 스튜디오(IDE)에서도 코드를 읽지 못하기 때문에 다음의 경로를 명시해야 한다. 45 | 46 | ``` 47 | build/generated/ksp/debug/kotlin 48 | ``` 49 | 50 | build.gradle.kts 예시 51 | 52 | ``` 53 | android { 54 | buildTypes { 55 | getByName("debug") { 56 | sourceSets { 57 | getByName("main") { 58 | java.srcDir(File("build/generated/ksp/debug/kotlin")) 59 | } 60 | } 61 | } 62 | } 63 | } 64 | ``` 65 | 66 | ## KSP 내부 살펴보기 67 | 68 | ![](https://github.com/google/ksp/raw/main/docs/ClassDiagram.svg) 69 | 70 | > **참고** [KSP API definition](https://github.com/google/ksp/blob/main/api/src/main/kotlin/com/google/devtools/ksp) 71 | > **참고** [KSP Symbol definition](https://github.com/google/ksp/blob/main/api/src/main/kotlin/com/google/devtools/ksp/symbol) 72 | 73 | KSP 모델에 대한 딥다이브를 해보자. 74 | 75 | KSP에서 타입에 대한 참조는 몇 가지 예외를 제외하면 명시적으로 지정하도록 되어있다. 76 | 77 | `KSFunctionDeclaration.returnType` 혹은 `KSAnnotation.annotationType`과 같이 타입을 참조하는 경우, 타입은 항상 annotation과 modifier가 포함된 `KSReferenceElement` 기반의 `KSTypeReference`이다. 78 | 79 | ```kotlin 80 | interface KSFunctionDeclaration : ... { 81 | val returnType: KSTypeReference? 82 | ... 83 | } 84 | 85 | interface KSTypeReference : KSAnnotated, KSModifierListOwner { 86 | val type: KSReferenceElement 87 | } 88 | ``` 89 | 90 | `KSTypeReference`는 Kotlin의 타입 시스템의 `KSType`으로 `resolve()`할 수 있고, Kotlin 문법과 일치하는 `KSReferenceElement`를 가지고 있다. 91 | 92 | 이번엔 `KSReferenceElement`다. 93 | 94 | 95 | ```kotlin 96 | interface KSReferenceElement : KSNode { 97 | val typeArguments: List 98 | } 99 | ``` 100 | 101 | `KSReferenceElement`는 유용한 정보를 많이 포함하고 있는 `KSClassifierReference` 혹은 `KSCallableReference`가 될 수 있다. 102 | 103 | ```kotlin 104 | interface KSClassifierReference : KSReferenceElement { 105 | val qualifier: KSClassifierReference? 106 | fun referencedName(): String 107 | 108 | override fun accept(visitor: KSVisitor, data: D): R { 109 | return visitor.visitClassifierReference(this, data) 110 | } 111 | } 112 | ``` 113 | 114 | 예를 들어 `KSClassifierReference`는 `referencedName`라는 속성을 가지고 있으며, 115 | 116 | ```kotlin 117 | interface KSCallableReference : KSReferenceElement { 118 | val receiverType: KSTypeReference? 119 | val functionParameters: List 120 | val returnType: KSTypeReference 121 | 122 | override fun accept(visitor: KSVisitor, data: D): R { 123 | return visitor.visitCallableReference(this, data) 124 | } 125 | } 126 | ``` 127 | 128 | `KSCallableReference`는 `receiverType`과 `functionArguments` 그리고 `returnType`을 가지고 있다. 129 | 130 | `KSTypeReference`에서 참조되는 타입의 선언이 필요한 경우 아래와 같은 순서로 접근한다. 131 | 132 | ```kotlin 133 | KSTypeReference -> .resolve() -> KSType -> .declaration -> KSDeclaration 134 | ``` 135 | 136 | `resolve()`를 통해 `KSType`으로 접근하고, `declaration` 속성을 통해 `KSDeclaration` 객체를 획득한다. 137 | 138 | ### Java Annotation Processing에 대응하는 KSP 레퍼런스 139 | 140 | 기존에 Annotation processor를 작성해 본 경험이 있다면 아래의 내용을 참조하면 좋다. 내용이 방대하여 링크로 대체한다. 141 | 142 | **참고** [Github ksp#referencs](https://github.com/google/ksp/blob/main/docs/reference.md) 143 | 144 | -------------------------------------------------------------------------------- /005. Kotlin Symbol Processing API/charlezz.md: -------------------------------------------------------------------------------- 1 | # About KSP.. 2 | 3 | ![pasted%2Bimage%2B0%2B%2834%29](https://1.bp.blogspot.com/-tl-oC11ymb0/YCLDMYK4v_I/AAAAAAAAQEM/C_qeIE53MZQTtRITP66rN83kwRIl_uEygCLcBGAsYHQ/s0/pasted%2Bimage%2B0%2B%252834%2529.png) 4 | 5 | ## 애노테이션과 애노테이션 프로세서 6 | 7 | **애노테이션(annotation)**은 소스 코드에 추가 할 수 있는 메타데이터의 한 형태로 컴파일러가 이를 참조 할 수 있도록 한다. 8 | 9 | **애노테이션 프로세서(annotation processor)**는 컴파일러의 플러그인의 일종으로 컴파일 타임에 애노테이션에 대한 코드베이스를 검사, 수정하거나 코드를 생성하는데 사용된다. 애노테이션 프로세서를 사용하는 대표적인 라이브러리로 Room, Dagger 등이 있다. 10 | 11 | ## kapt 12 | 13 | 코틀린 프로젝트를 컴파일 할 때는 javac가 아닌 kotlinc로 컴파일을 하기 때문에 Java 애노테이션 프로세서가 동작하지 않는다. 그렇기 때문에 코틀린에서는 애노테이션을 처리하기 위해 **kapt(kotlin annotation processing tool)**라는 것을 사용한다. 코틀린 프로젝트에서 kapt를 사용하기 위해서 build.gradle 파일에 다음과 같은 라인을 추가하면 된다. 14 | 15 | ```groovy 16 | apply plugin: 'kotlin-kapt' 17 | ``` 18 | 19 | 또한 기존 annotationProcessor 키워드를 다음과 같은 예시처럼 **kapt**로 변경해야 한다 20 | 21 | ```groovy 22 | dependencies{ 23 | //annotationProcessor "com.google.dagger:hilt-android-compiler:$hilt_version" 24 | kapt "com.google.dagger:hilt-android-compiler:$hilt_version" 25 | } 26 | ``` 27 | 28 | ## KSP란? 29 | 30 | KSP(Kotlin Symbol Processing)는 코틀린에서 경량화 된 컴파일러 플러그인을 개발할 수 있는 API다. KSP는 코틀린 1.4.30 버전 이상부터 호환 되며, [KSP Github 리포지토리](https://github.com/google/ksp)에서 오픈 소스 코드 및 문서를 확인할 수 있다. 31 | 32 | ### Why KSP? 33 | 34 | 코틀린 코드를 컴파일 할 때 가장 큰 문제 중 하나는 코틀린 용 애노테이션 프로세싱 시스템이 없다는 점이다. 안드로이드 개발에서 Room, Dagger 등과 같은 라이브러리는 필수적이며, 이러한 라이브러리들은 kapt를 통한 자바 애노테이션 프로세서에 의존한다. 문제는 kapt가 자바 애노테이션 프로세싱을 위해 중간에 자바 Stub들을 생성한다는 점이다. Stub 생성 비용은 전체 코드 분석의 대략 1/3정도의 비용이 들어간다. 35 | 36 | ### KSP의 특징 37 | 38 | KSP는 코틀린 코드를 직접 파싱하기 위해 강력하면서도 간단한 API를 제공하기 때문에 빌드시간을 단축할 수 있다. 실제로 Room 라이브러리를 기준으로 벤치마킹했을 때, KSP사용시 kapt보다 약 2배 빨라지는 결과를 확인할 수 있다. Glide와 같은 일부 프로세서의 경우전체 컴파일 시간을 최대 25%까지 줄인다. 39 | 40 | KSP는 컴파일러 변경사항을 숨기도록 설계되어 이를 사용하는 프로세서의 유지비용을 최소화 한다. 그러나 컴파일러 또는 코틀린 언어의 주요 변경 사항은 여전히 API를 사용하는 사용자에게 노출되어야 한다. 41 | 42 | KSP는 JVM에 의존하지 않도록 설계되어 향후 다른 플랫폼에 보다 쉽게 적용할 수 있다. 43 | 44 | **kotlinc 컴파일러 플러그인과의 비교** 45 | KSP는 일반적인 사례들을 해결하기 위해 단순한 API를 제공하고, 이 기능들은 kotlinc 플러그인의 하위 집합일 뿐이다. kotlinc는 표현식과 구문을 검사하고 코드를 수정할 수도 있지만 KSP는 할 수 없다. 46 | 47 | **리플렉션과의 비교** 48 | KSP의 API는 kotlin.reflect와 유사하다. 이들 간의 주요 차이점은 KSP의 타입 참조를 명시적으로 확인해야 한다는 것이다. 이것이 이 둘간에 인터페이스가 공유되지 않는 이유 중 하나다. 49 | 50 | **다음은 KSP의 목표가 아님** 51 | 52 | - 소스코드의 표현식 수준의 정보들을 검토 53 | - 소스코드를 수정 54 | - Java Annotation Processing API와 100% 호환 55 | - IDE 통합 -------------------------------------------------------------------------------- /005. Kotlin Symbol Processing API/namhoon_1.md: -------------------------------------------------------------------------------- 1 | ## Annotation Processor란 무엇인가? 2 | 3 | Kotlin Symbol Processing API(이하 `KSP`)를 분석하기 전에, `KSP`의 전신이라고 할 수 있는 `Annotation Processor`에 대해서 분석해보자. 4 | 5 | `Annotation Processor`이란 용어는 낯설더라도, 아래와 같은 코드들은 개발하면서 종종 보았을 것이다. 6 | 7 | ```java 8 | @Overide 9 | @NonNull 10 | @Nallable 11 | ``` 12 | 13 | 이러한 형식을 `Annotation`이라고 부르며, 컴파일 타임에 컴파일러에게 특정 정보를 전달하거나, 미리 지정된 코드를 생성하기 위한 용도로 사용된다. 14 | 15 | 비단 Android 뿐만 아니라 Spring Framework 등을 개발할때에도 자주 활용하게 되는 문법이다. 16 | 17 | Java의 공식 문서의 Annotation 정의는 아래와 같다. 18 | 19 | Annotation은 메타데이터의 한 형태로 프로그램에 대한 데이터를 제공하는 데 사용되며, 코드의 동작이 직접적인 영향을 주지 않는다. 20 | 21 | Annotation은 크게 3가지 용도로 쓰인다. 22 | 23 | 1. **Information for the compiler** : 컴파일러가 에러를 탐지하거나, 경고를 표시하지 않도록 사전에 정보를 전달한다. 24 | 2. **Compile-time and deployment-time processing** : 코드나 xml 파일 등을 컴파일 타임에 생성할 수 있도록 처리한다. 25 | 3. **Runtime processing** : 몇몇 annotation들은 런타임에도 검사를 수행하도록 처리해준다. 26 | 27 | 전문은 아래 링크를 참조하자. 28 | 29 | > **참고** [Oracle JavaDoc#Annotations](https://docs.oracle.com/javase/tutorial/java/annotations/index.html) 30 | 31 | 32 | 특히 구글에서 제공되는 `androidx.annotation`을 참조하면 매우 다양한 Annotation을 활용할 수 있다. 33 | 34 | `androidx.annotation` 패키지에 속한 annotation 리스트들은 아래의 링크를 참고하자. 35 | 36 | > **참고** [Android Developers#androidx.annotation](https://developer.android.com/reference/androidx/annotation/package-summary) 37 | 38 | 39 | Annotation은 Java 5부터 지원하고 있으며, `AbstractProcessor`클래스를 상속받아 구현할 수 있다. 40 | 41 | > 구현 예제를 추가하는 것이 좋을까? 42 | > 추가하는 것이 좋다면 예시로 하나 작업해서 Deep Dive 해보도록 하겠음. 43 | 44 | ### Android에서 Annotation Processor를 사용하는 라이브러리들 45 | 46 | #### Room 47 | 48 | Android에서 SQLite에 대한 추상화를 제공하는 Room 라이브러리에도 Annotation Processor가 적용되어 있다. 49 | 50 | 아래는 대표적인 예제인 User 관련 코드이다. 51 | 52 | ```kotlin 53 | // User.kt 54 | @Entity 55 | data class User( 56 | @PrimaryKey val uid: Int, 57 | @ColumnInfo(name = "first_name") val firstName: String?, 58 | @ColumnInfo(name = "last_name") val lastName: String? 59 | ) 60 | ``` 61 | 62 | ```kotlin 63 | // UserDao.kt 64 | @Dao 65 | interface UserDao { 66 | @Query("SELECT * FROM user") 67 | fun getAll(): List 68 | 69 | @Query("SELECT * FROM user WHERE uid IN (:userIds)") 70 | fun loadAllByIds(userIds: IntArray): List 71 | 72 | @Query("SELECT * FROM user WHERE first_name LIKE :first AND " + 73 | "last_name LIKE :last LIMIT 1") 74 | fun findByName(first: String, last: String): User 75 | 76 | @Insert 77 | fun insertAll(vararg users: User) 78 | 79 | @Delete 80 | fun delete(user: User) 81 | } 82 | ``` 83 | 84 | ```kotlin 85 | // AppDatabase.kt 86 | @Database(entities = arrayOf(User::class), version = 1) 87 | abstract class AppDatabase : RoomDatabase() { 88 | abstract fun userDao(): UserDao 89 | } 90 | ``` 91 | 92 | 위의 에졔에서 쓰인 Annotation들은 `@Entity`, `@PrimaryKey`, `@ColumnInfo`, `@Dao`, `@Query`, `@Insert`, `@Delete`이다. 93 | 94 | 이 Annotation들의 구현체를 확인해보자. 95 | 96 | #### Entity.java 97 | 98 | ```java 99 | @Target(ElementType.TYPE) 100 | @Retention(RetentionPolicy.CLASS) 101 | public @interface Entity { 102 | String tableName() default ""; 103 | Index[] indices() default {}; 104 | boolean inheritSuperIndices() default false; 105 | String[] primaryKeys() default {}; 106 | ForeignKey[] foreignKeys() default {}; 107 | String[] ignoredColumns() default {}; 108 | } 109 | ``` 110 | 111 | #### PrimaryKey.java 112 | 113 | ```java 114 | @Target({ElementType.FIELD, ElementType.METHOD}) 115 | @Retention(RetentionPolicy.CLASS) 116 | public @interface PrimaryKey { 117 | boolean autoGenerate() default false; 118 | } 119 | ``` 120 | 121 | #### ColumnInfo.java 122 | ```java 123 | @Target({ElementType.FIELD, ElementType.METHOD}) 124 | @Retention(RetentionPolicy.CLASS) 125 | public @interface ColumnInfo { 126 | String name() default INHERIT_FIELD_NAME; 127 | @SuppressWarnings("unused") @SQLiteTypeAffinity int typeAffinity() default UNDEFINED; 128 | boolean index() default false; 129 | String defaultValue() default VALUE_UNSPECIFIED; 130 | String INHERIT_FIELD_NAME = "[field-name]"; 131 | 132 | 133 | int UNDEFINED = 1; 134 | int TEXT = 2; 135 | int INTEGER = 3; 136 | int REAL = 4; 137 | int BLOB = 5; 138 | @IntDef({UNDEFINED, TEXT, INTEGER, REAL, BLOB}) 139 | @Retention(RetentionPolicy.CLASS) 140 | @interface SQLiteTypeAffinity { 141 | } 142 | 143 | int UNSPECIFIED = 1; 144 | int BINARY = 2; 145 | int NOCASE = 3; 146 | int RTRIM = 4; 147 | @RequiresApi(21) 148 | int LOCALIZED = 5; 149 | @RequiresApi(21) 150 | int UNICODE = 6; 151 | @IntDef({UNSPECIFIED, BINARY, NOCASE, RTRIM, LOCALIZED, UNICODE}) 152 | @Retention(RetentionPolicy.CLASS) 153 | @interface Collate { 154 | } 155 | String VALUE_UNSPECIFIED = "[value-unspecified]"; 156 | } 157 | ``` 158 | 159 | 이 외에도 `@Dao`, `@Query`, `@Insert`, `@Delete`과 같은 Annotation들은 각자 인터페이스, 구현체 값들을 이미 가지고 있다. 160 | 161 | 비단 **Room** 뿐만 아니라 **Butterknife**, **Dagger**, **Retrofit** 같은 라이브러리들도 Annotation을 기반으로 동작한다. 162 | 163 | 그렇다면 이미 상용화되어 쓰이고 있는 라이브러리말고 직접 만들어 쓸 수는 없는 것일까? 164 | 165 | Java 기반의 Android도 물론 지원가능하지만 Kotlin 기반의 **KAPT** 를 이용해 작성할 수 있다. 166 | 167 | ### KAPT (Kotlin Annotation Processing Tool) 168 | 169 | KAPT는 위의 제목의 약자에서 알 수 있듯, 코틀린에서 생성한 코드를 참조하기 위해 추가해야하는 의존성이다. 170 | 171 | 다만 이 KAPT는 Kotlin 코드를 Java Annotation Processor를 수정하지 않기 위해 컴파일시 Java로 된 Stub을 생성하게 된다. 172 | 173 | Stub을 생성하는 것은 kotlinc의 분석 비용의 3분의 1이나 차지하므로, 빌드시 많은 오버헤드가 발생하게 된다. 174 | 175 | 따라서, 이 컴파일 타임을 획기적으로 줄여주는 솔루션이 나오게 되니, 이게 바로 이번 주제에서 다루게 될 KSP이다. 176 | 177 | 다음 포스트에서 KSP에 대해 자세히 살펴보도록 하자. 178 | 179 | ## References 180 | - https://developer.android.com/reference/androidx/annotation/package-summary 181 | - https://hannesdorfmann.com/annotation-processing/annotationprocessing101/ 182 | - https://kotlinlang.org/docs/kapt.html -------------------------------------------------------------------------------- /005. Kotlin Symbol Processing API/siyoung.md: -------------------------------------------------------------------------------- 1 | ## Kapt 와 KSP 살펴보기 2 | 3 | 4 | ## 어노테이션 프로세싱(Annotation Processing) 5 | 6 | JSR 269 는 자바 컴파일러, 어노테이션 프로세스를 위한 특별한 종류의 플러그인을 위한 API를 정의한다. 이러한 플러그인은 컴파일러에게 어떠한 코드 요소(클래스, 메소드, 필드)가 @foo로 주석되어 있는지 물어볼 수 있다. 컴파일러는 주석된 요소를 나타내는 객체의 콜렉션을 반환한다. 그 후 프로세서는 콜렉션을 검사하고 주석 추가되어 있는 코드와 동일한 단계에서 컴파일 될 새로운 코드를 생성한다. 이렇게 생성된 코드는 컴파일러가 동작을 시작할 때 존재하지 않더라도 손으로 직접 작성한 코드를 사용할 수 있게 된다. 어노테이션을 통해 직접 작성한 코드 기반으로 새로운 코드가 작성될 수 있고 손쉽게 기능을 구현하고 사용할 수 있게 된다. 7 | 8 | ## 어노테이션 설정 9 | 10 | 자바 이외의 언어에서 어노테이션을 지원하기 위해서는 몇가지 옵션이 있다. 11 | 12 | ### 1. JSR 269 API 을 다시 구현 13 | 14 | JSR 269 를 다시 구현하는것은 약간의 작업이 필요하지만 어렵지는 않다. 문제는 자바와 코틀린이 혼합된 프로젝트에서는 도움이 되지 않는다. 자바와 코틀린 모든 코드의 요소에 주석을 처리해야하며, 코틀린에서만 JSR 269 지원하는 것은 큰 이득이 아니다. 15 | 16 | ### 2. Groovy 사용 17 | 18 | 코틀린 소스로부터 자바 소스로 완벽히 변환하여 프로세스를 실행중인 자바 컴파일러에 전달하는 것은 매우 힘들다. 그러나 실제 필요한 선언만 해주면 Groovy 는 코드를 변환하고 컴파일러에 전달해준다. Groovy 코드로부터 자바 스텁(stubs) 을 생성하고 자바 컴파일러에 전달한다. 코틀린에서도 작동은 하지만, 컴파일러가 두번 실행된다. 첫번째는 스텁을 생성하고, 두번째는 생성된 코드를 완전히 컴파일 한다. 프로세스에 생성된 클래스를 참조할 수 있지만, 경우에 따라서 문제가 발생할 수 있다. 예를 들면 스텁을 생성하는 동안 우항(right-hand-side expression) 에 추론된 프로퍼티(property)/펑션(function) 타입을 생성된 코드에 사용할 때 경우에 따라 문제가 발생할 수 있다. 19 | 20 | ### 3. Kapt(Kotlin Annotation Processing) 사용 21 | 22 | 코틀린 바이너리를 자바코드라고 가정한다. 일반적으로 코틀린 컴파일러가 먼저 실행되고 자바 컴파일러는 바이너리 .class 파일로 코틀린 코드를 인식된다. 물론 소스와 바이너리의 모든 코드 요소는 자바 컴파일러 내에서 균일하게 표현됩니다. 따라서 주석 처리된 자바 소스만 가져오는 대신에 프로세서는 또한 주석처리된 코틀린 바이너리 가져올 수 있지만 사용가능한 API 를 통해서 차이를 알 수 없다. 안타깝게도 Javac 자동적으로 실행되지 않지만, 자바 컴파일러와 어노테이션 프로세서 사이를 연결하고 바이너리 요소를 찾아서 javac에서 일반적으로 반환하는 소스 요소에 추가할 수 있다. 큰 장점은 솔루션이 구현하기 쉽다. ~~하지만 몇 가지 중요한 제한 사항이 있다. 코틀린 코드는 프로세서에 의해 생성된 선언을 참조할 수 없고 소스에 주석은 바이너리를 통해 표시되지 않는다.~~ 23 | 24 | ## Kapt 설정 및 사용 방법 25 | 26 | Gradle plugin 에서 kotlin-kapt 를 활성화 할 수 있다. 27 | 28 | - Groovy 에서 설정 29 | plugins { 30 | id “org.jetbrains.kotlin.kapt” version “1.5.20” 31 | } 32 | 33 | - Kotlin 에서 설정 34 | plugins { 35 | kotlin("kapt") version "1.5.20" 36 | } 37 | 38 | - apply plugin 문법을 적용해서 설정 39 | apply plugin: 'kotlin-kapt' 40 | 41 | 그 다음에 각각의 의존성을 kapt 설정을 사용해서 dependencies 에 추가한다. 42 | 43 | dependencies { 44 | kapt 'groupId:artifactId:version' 45 | } 46 | 47 | dependencies { 48 | implementation "com.google.dagger:hilt-android:2.28-alpha" 49 | kapt "com.google.dagger:hilt-android-compiler:2.28-alpha" 50 | } 51 | 52 | 만약에 Android support 를 사용하기 위해 어노테이션 프로세서를 사용했다면, annotationProcessor 설정을 kapt 로 변경한다. 만약 프로젝트에 자바 클래스가 포함되어 있다면, kapt 가 그 또한 처리한다. 53 | 54 | 만약 androidTest 또는 test 소스를 위한 어노테이션 프로세서를 사용한다면, 각각 kaptAndroidTest 와 kaptTest 이름으로 kapt 설정을 한다. kaptAndroidTest 와 kaptTest 는 kapt 를 확장한 것이며, kapt 의존성을 제공할 수 있고 프로덕션 소스와 테스트 모두 어노테이션 프로세서를 사용할 수 있게 한다. 55 | 56 | arguments{} 블록을 사용해서 어노테이션 프로세서에 인자를 전달할 수 있다. 57 | 58 | kapt { 59 | arguments { 60 | arg("key", "value") 61 | } 62 | } 63 | 64 | kapt 어노테이션 프로세싱 작업은 gradle 안에서 기본적으로 캐시된다. 그러나 어노테이션 프로세서가 입력에서 출력으로 변환이 필요 없는 임의의 코드 실행하고, gradle 등에서 추적하지 않는 파일에 접근하고 수정할 수 있다. 어노테이션 프로세서가 적절한 캐시를 빌드하지 못하는 경우, 캐시를 하지 않도록 설정할 수 있다. 65 | 66 | kapt { 67 | useBuildCache = false 68 | } 69 | 70 | kapt 로 늘어나는 빌드 시간을 개선하기 위해 gradle compile avoidance 를 할 수 있다. compile avoidance 가 활성화 되어 있다면, gradle 은 프로젝트를 리빌딩할 때 어노테이션 프로세싱을 건너 뛸 수 있다. 71 | 특히 다음과 같은 때 어노테이션 프로세싱은 건너 뛴다. 72 | 73 | * 프로젝트의 소스 파일이 변경되지 않았을 때 74 | 75 | * 종속성의 변경이 ABI 와 호환되는 경우 76 | ex) 메소드 바디내의 변경만 있는 경우 77 | 78 | 그러나 어노테이션 프로세서는 컴파일 클래스패스 안에서 발견되는 어떠한 변경에 어노테이션 프로세싱 작업이 수행되므로 compile avoidance 사용할 수 없다. 79 | compile avoidance 와 함께 kapt 를 실행하려면 아래와 같이 설정한다. 80 | 81 | * 어노테이션 프로세서 의존성에 kapt* 를 추가한다. 82 | 83 | 컴파일 클래스패스에서 어노테이션 프로세서의 탐색을 비활성화 하기 위해서는 gradle.properties 파일에 아래와 같은 설정을 추가한다. 84 | 85 | kapt.include.compile.classpath=false 86 | 87 | kapt 는 incremental compilation 을 기본적으로 활성화된다. incremental compilation 는 소스 파일과 빌드 사이에 변경을 추적하고, 오직 변경에 영향을 받는 파일만 컴파일하도록 한다. 88 | 89 | incremental compilation 비활성화 하기 위해서는 gradle.properties 파일에 아래와 같이 설정을 추가한다. 90 | 91 | kapt.incremental.apt=false 92 | 93 | kapt 에서 어노테이션 프로세서 실행을 위해 자바 컴파일러를 사용할 수 있다. javac 에 임의의 옵션을 전달하는 방법은 아래와 같이 설정한다. 94 | 95 | kapt { 96 | javacOptions { 97 | // Increase the max count of errors from annotation processors. 98 | // Default is 100. 99 | option("-Xmaxerrs", 500) 100 | } 101 | } 102 | 103 | AutoFactory 와 같은 일부 어노테이션 프로세서는 선언된 서명에 정확한 타입에 의존한다. 기본적으로 kapt 는 알수없는 모든 타입을 NonExistentClass 로 변경한다. 이를 막기 위해 스텁에서 추론 타입 에러를 활성화할 수 있다. build.gradle 파일에 아래와 같이 설정할 수 있다. 104 | 105 | kapt { 106 | correctErrorTypes = true 107 | } 108 | 109 | kapt 는 코틀린 소스를 생성할 수 있다. 생성된 코틀린 소스파일을 processingEnv.options["kapt.kotlin.generated"] 에 지정된 경로를 작성하면, 이 파일들은 메인소스와 함께 컴파일된다. 110 | 111 | 기본적으로 kapt 는 모든 어노테이션 프로세서 실행하고 javac 에 의해 어노테이션 프로세서를 비활성화 한다. 그러나 javac 의 어노테이션 프로세서 작업이 필요할 수도 있다. gradle 빌드 파일에서 keepJavacAnnotationProcessors 옵션을 사용한다. 112 | 113 | kapt { 114 | keepJavacAnnotationProcessors = true 115 | } 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | [https://kotlinlang.org/docs/kapt.html](https://kotlinlang.org/docs/kapt.html) 124 | 125 | [https://blog.jetbrains.com/kotlin/2015/05/kapt-annotation-processing-for-kotlin/](https://blog.jetbrains.com/kotlin/2015/05/kapt-annotation-processing-for-kotlin/) 126 | 127 | [https://github.com/google/ksp/blob/main/docs](https://github.com/google/ksp/blob/main/docs) 128 | 129 | -------------------------------------------------------------------------------- /005. Kotlin Symbol Processing API/toc.md: -------------------------------------------------------------------------------- 1 | ## Kotlin Symbol Processing API TOC 2 | 3 | ### Part 1 - Annotation Processor란 무엇인가? 4 | - Annotation 사용하는 라이브러리 예시 5 | - Room / Butterknife / Dagger2 6 | - KAPT란? 7 | - Kotlin에서 KAPT 사용시 단점 8 | - Stub 생성으로 인한 빌드 속도 이슈 9 | - 해결 방안 10 | - KSP 사용 11 | 12 | ### Part 2 - Kotlin Symbol Processing API 13 | - KSP란 무엇인가? 14 | - KSP 왜? 15 | - 장점 16 | - 특징 17 | - 차이점 18 | - 제약사항(Limitation) 19 | 20 | ### Part 3 - KSP Example 21 | - IntentBuilder(+Dynamic Feature Module) (수환) 22 | - WebView Setting 23 | - Logging 24 | - AppLink(DeepLink) 25 | - @Parcelize 26 | - Bitmap, Lottie 27 | - https://medium.com/@jsuch2362/my-first-kotlin-symbol-processing-tool-for-android-4eb3a2cfd600 28 | -------------------------------------------------------------------------------- /005. Kotlin Symbol Processing API/ukhyun-part1.md: -------------------------------------------------------------------------------- 1 | # Kotlin Symbol Processing API 2 | 3 | ## Part1 - Annotation Processor란 무엇인가? 4 | 5 | ### Annotation Processor 6 | 7 | 자바나 코틀린으로 개발할 때 종종 클래스나 메소드, 변수 등에 @Override, @Nullable 등의 @ 이 붙은걸 본 적이 있을 것이다. 일종의 라벨을 붙여주는 의미인데, 이참에 제대로 살펴보자. 8 | 9 | > Annotations, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate. 10 | 11 | 오라클 문서에서 Annotation에 대해서 설명하는 부분을 해석해보면 어노테이션은 일종의 메타데이터의 형태이고, 프로그램 그 자체가 아닌 프로그램에 대한 정보를 제공한다. 또 Annotation이 달린 코드의 동작에 직접적인 영향을 미치지 않는다. 여기서 Annotation이란 '@' 문자 뒤에 뒤따르는 문자열을 말한다. Annotation은 다음과 같은 용도가 있는데 12 | 13 | - compiler가 오류 혹은 경고를 찾아내는 데 사용할 수 있다. 14 | - Annotation information을 처리하여 코드나 xml 파일등을 생성할 수 있다. 15 | - 일부 Annotation은 runtime에 검사할 수 있다. 16 | 17 | - Annotation은 Element를 가질 수 있는데, 이름을 지정하거나 해당 요소에 대한 값이 있을 수 있다. 또한 element가 하나이면 생략할 수도 있고 element가 아예 없다면 괄호도 생략 할 수 있다. 18 | 19 | - 여러개의 Annotation을 갖는것을 repeating annotation이라 한다. 20 | 21 | - Java SE 8 부터 Annotation은 Type을 지정할 때도 적용할 수 있다. 22 | 23 | ```java 24 | myString = (@Nonnull String) strl; 25 | ``` 26 | 27 | 28 | 29 | ### Predefined Annotation Type 30 | 31 | Java.lang package에 자바 언어에서 제공하는 Annotation들이 있다. 몇 가지 살펴보자. 32 | 33 | - @Deprecated : deprecated 됐음을 의미하며 더 이상 쓰지 말 것을 권장할 때 사용한다. 이 Annotation이 달린 코드를 사용하면 컴파일러는 경고를 내뱉는다. 34 | 35 | - @SuppressWarnings : 이 Annotation은 컴파일러가 생성할 경고를 억제하도록 지시한다. 36 | 37 | ```java 38 | @SuppressWarnings("deprecation") 39 | void useDeprecatedMethod() { 40 | // deprecation warning 41 | // - suppressed 42 | objectOne.deprecatedMethod(); 43 | } 44 | ``` 45 | 46 | Java.lang.annotation package에 있는 Annotation을 알아보자. 해당 패키지에는 다른 Annotation에 적용될 수 있는 Annotation들이 있으며 이를 메타 Annotation이라 한다. 47 | 48 | - @Retention : Retention Annotation은 표기된 Annotation이 저장되는 방법을 지정한다. 49 | - RetentionPolicy.SOURCE : source level에서만 유지되며 Compiler에서 무시된다. 50 | - RetentionPolicy.CLASS : compile time에 Compiler에 의해 유지되지만, JVM에서는 무시된다. 51 | - RetentionPolicy.RUNTIME : JVM에 의해서 유지되므로 runtime에서 사용가능하다. 52 | - @Documented : Annotation이 사용될 때 마다 해당 element가 Javadoc에 문서화 되어야 함을 나타낸다. 53 | - @Target : Annotation을 적용할 수 있는 Java Elements 종류를 제한한다. 54 | - @Inherited : super class 로부터 상속될 수 있는 Annotation 타입이다. class 선언시에만 적용. 55 | 56 | 57 | 58 | ### KAPT(Kotlin Annotation Processing Tool) 59 | 60 | 먼저 JSR 269 얘기를 해보자. JSR 269는 Annotation Processor를 위한 특별한 자바 컴파일러 API 이다. 이러한 플러그인은 "@Foo로 주석 처리 된 코드(클래스, 메서드, 필드)는 무엇인가?" 라고 컴파일러에게 물어볼 수 있고, 컴파일러는 Annotation Processing(@Foo)이 달린 요소를 가진 컬렉션을 반한한다. 이후, Processor는 이를 검사하고 Annotation이 추가된 코드와 동일한 단계에서 컴파일 될 **새로운 코드**를 생성할 수 있다. 61 | 62 | Annotation Processing을 Java 이외의 언어에서 제공하기 위해 몇 가지 옵션이 있을 수 있는데 해당 블로그 글을 참고하자. 이 중 한 가지 옵션을 선택하여 **kapt**라고 이름을 붙이게 되었고, 이 옵션만 살펴보자. 63 | 64 | 이 옵션은 Kotlin 바이너리가 자바 소스라고 가정하는 것이다. 일반적으로 코틀린 컴파일러가 먼저 실행되고 자바 컴파일러는 코틀린 코드를 바이너리 .class 파일로 인식한다. 따라서 Annotation이 달린 자바 소스만 가져오는 대신, Processor는 Annotation이 달린 코틀린 바이너리를 가져올 수 있지만 사용 가능한 API를 통해 차이를 인식하지 못한다. 그러나 자바 컴파일러는 이를 자동으로 수행하지는 않고, 자바 컴파일러와 Annotation Processor 사이를 연결하여 바이너리 요소를 직접 찾아 javac에서 일반적으로 반환하는 소스 요소에 추가할 수 있다. 65 | 66 | 이러한 방식의 큰 장점은 약간의 바이트코드를 생성하긴 하지만 구현이 쉽다는 점이다. 67 | 68 | Kotlin에서는 Annotation Processor를 위해 **kapt**라는 컴파일 플러그인을 지원한다. 간단히 말해 kapt를 통해서 Dagger나 Databinding을 코틀린 프로젝트에서 쓸 수 있는 것이다. 공식 홈페이지에서 kapt를 사용하기 위한 설정들을 살펴보자. 만약 Annotation Processor를 쓰기 위해 Android Support를 사용하고 있다면 annotationProcessor를 kapt로 전환하면 된다. 자바 클래스도 지원이 된다. 69 | 70 | ```groovy 71 | apply plugin: 'kotlin-android' 72 | apply plugin: 'kotlin-kapt' 73 | ``` 74 | 75 | 이 때 kotlin-android 설정을 먼저 해줘야 kotlin-kapt를 쓸 수 있다. 76 | 77 | 78 | 79 | ### Reference 80 | 81 | - https://docs.oracle.com/javase/tutorial/java/annotations/index.html 82 | - https://imstudio.medium.com/kotlin-kotlin-plugin-should-be-enabled-before-kotlin-kapt-d5879f45f09d 83 | - https://kotlinlang.org/docs/kapt.html#using-in-gradle 84 | - https://www.charlezz.com/?p=1167 85 | - https://blog.jetbrains.com/kotlin/2015/05/kapt-annotation-processing-for-kotlin/ 86 | - https://medium.com/@workingkills/pushing-the-limits-of-kotlin-annotation-processing-8611027b6711#id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImI2ZjhkNTVkYTUzNGVhOTFjYjJjYjAwZTFhZjRlOGUwY2RlY2E5M2QiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJuYmYiOjE2MjU2NDU5NTcsImF1ZCI6IjIxNjI5NjAzNTgzNC1rMWs2cWUwNjBzMnRwMmEyamFtNGxqZGNtczAwc3R0Zy5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjEwODA0NTQ0MzA1Njg1NTUxMTcyNiIsImVtYWlsIjoidWtoeXVuOTFAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF6cCI6IjIxNjI5NjAzNTgzNC1rMWs2cWUwNjBzMnRwMmEyamFtNGxqZGNtczAwc3R0Zy5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsIm5hbWUiOiLsmrHtmIQiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EtL0FPaDE0R2dTdmExaU9VeEEtTFJHbHFhTE1uby11d1E2emhVU01SekUwaXNnamc9czk2LWMiLCJnaXZlbl9uYW1lIjoi7Jqx7ZiEIiwiaWF0IjoxNjI1NjQ2MjU3LCJleHAiOjE2MjU2NDk4NTcsImp0aSI6IjY0NTBmZGU3ZTcyZDVmZTJiZmFlMWY0YjdiYTVhYTliYmU5NTRlZjYifQ.R3LNpWB5dPc2tKzQSdN_W12Z4q1SCD7mFPecGyiShxHfohSatHbGEKgdanBEEKzCuD8Jhxg6bFRXTbliGDB6GzKpWbqommoiUbpMH6xkPDiWP_k1RETpewj9Mi4j5-UNZ9dI1-XrqUC8-cIw9qPvGYy5lJV78KIsac159EgskLb-wW8J5AAFCnt8KJyCnHYNI_H1rwd4XJbmj2rb5CMLFrJs7XxUChG60J59ye_lPgZCp0FPUWSAQzYuroGXHedG0F_9H4y4m6JmH_Y9UnEXXgdC20QOVL9t-pWKHFvmIqYHZPMTyP66Mnay_w8TSf5-jjV8jLaH90_7OZyYkW2FXQ -------------------------------------------------------------------------------- /005. Kotlin Symbol Processing API/ukhyun-part2.md: -------------------------------------------------------------------------------- 1 | # Kotlin Symbol Processing API 2 | 3 | ## Part2 Kotlin Symbol Processing API 4 | 5 | ### KSP 6 | 7 | > Kotlin Symbol Processing (KSP) is an API that you can use to develop lightweight compiler plugins. KSP provides a simplified compiler plugin API that leverages the power of Kotlin while keeping the learning curve at a minimum. Compared to KAPT, annotation processors that use KSP can run up to 2x faster. 8 | 9 | KSP는 경량 컴파일러 플러그인을 개발하는 데 사용할 수 있는 API이다. KSP는 최소한의 러닝커브로 Kotlin의 기능을 활용하는 단순화 된 컴파일러 플러그인 API 를 제공한다. KAPT에 비해 KSP를 사용하는 Annotation Processor는 최대 2배 빠르게 실행할 수 있다. 10 | 11 | KSP API는 코틀린의 특성 - 확장 함수, 로컬 함수 같은 기능을 이해한다. 또한 KSP는 타입을 명시적으로 다루고, 기본적인 타입 검사와 동등성 검사를 지원한다. 12 | 13 | API는 코틀린 문법에 따라 symbol 수준에서 코틀린 프로그램 구조를 모델링 한다. KSP 기반 플러그인이 소스 프로그램을 처리할 때 클래스, 클래스 멤버, 함수 및 관련 매개 변수와 같은 구성은 프로세서에서 쉽게 접근할 수 있기 때문에 코틀린 개발자들에게 편리하다. 14 | 15 | 개념적으로 KSP는 Kotlin 리플렉션의 KType과 유사하다. API 를 사용하면 프로세서가 클래스 선언에서 특정 타입 인자가 있는 해당 타입, 또는 그 반대로 탐색할 수 있다. 16 | 17 | KSP를 생각하는 또 다른 방법은 코틀린 프로그램의 전처리기 프레임워크이다. 만약 KSP 기반 플러그인을 symbol processors 혹은 단순히 processors라고 한다면, 컴파일 타임에서 데이터 흐름은 다음과 같습니다. 18 | 19 | 1. Processor는 소스 프로그램과 리소스를 읽고 분석 20 | 2. Processor는 코드 또는 다른 형태의 출력을 생성 21 | 3. Kotlin 컴파일러는 생성된 코드와 함께 소스 프로그램을 컴파일함 22 | 23 | Full-Fledged 컴파일러 플러그인과 달리, Processor는 코드를 수정할 수 없다. 언어 의미를 변경하는 컴파일러 플러그인은 때때로 매우 혼란스러울 수 있는데, KSP는 소스 프로그램을 읽기 전용으로 처리하여 이를 방지한다. 24 | 25 | ### Why KSP 26 | 27 | 컴파일러 플러그인은 코드 작성 방법을 크게 향상시킬 수 있는 강력한 메타프로그래밍 도구이다. 컴파일러 플러그인은 컴파일러를 라이브러리로 직접 호출하여 입력 프로그램을 분석하고 편집한다. 이러한 플러그인은 다양한 용도로 쓰일 수 있는 산출물을 생성한다. 예를 들어, bolierplate 코드를 생성할 수 있고 Parcelble과 같은 특별히 마크된 프로그램 요소에 대한 전체 구현을 생성할 수도 있다. 플러그인은 다양한 요옫로 사용되며 언어로 직접 제공되지 않는 기능을 구현하고 미세 조정하는 데 사용할 수도 있다. 28 | 29 | 컴파일러 플러그인은 강력하지만, 역시 대가는 따른다. 가장 간단한 플러그인을 작성하려고만 해도, 컴파일러 배경 지식이 있어야 하며 특정 컴파일러의 구현 세부 사항에 대해 어느 정도 익숙해져야 한다. 또 다른 문제는 플러그인이 특정 컴파일러 버젼과 밀접하게 연관되어 있다는 점이다. 즉 최신 버젼의 컴파일러를 지원할 때 마다 플러그인을 업데이트 해야 할 수도 있다. 30 | 31 | KSP는 컴파일러 변경 사항을 숨기도록 설게되어 KSP를 사용하는 Processor의 유지 보수가 최소화 된다. KSP는 또한 JVM에 종속되지 않도록 설계되어 향후 다른 플랫폼에 쉽게 적응할 수 있기도 하다. KSP는 빌드 시간을 최소화 시키도록 설계되었다. Glide 와 같은 일부 Processor의 경우 KSP는 KAPT와 비교할 때 컴파일 시간을 25%까지 줄인다. 32 | 33 | KSP는 자체적으로 컴파일러 플러그인으로 구현된다. 34 | 35 | ### Comparison to KAPT 36 | 37 | KAPT는 많은 양의 자바 Annotation Processor 코틀린 프로그램에서 사용할 수 있는 솔루션이다. KAPT에 비해 KSP의 주요 장점은 JVM과 관련이 없는 향상된 빌드 성능, 보다 관용적인 Kotlin API 및 Kotlin symbol을 이해하는 기능이다. 38 | 39 | 자바 Annotation Processor를 수정하지 않고 실행하기 위해 KAPT는 코틀린 코드를 자바 Annotation Processor가 관심을 갖는 정보를 유지하는 자바 스텁으로 컴파일한다. 이러한 스텁을 만들기 위해 KAPT가 코틀린 프로그램의 모든 symol을 확인해야 한다. 스텁 생성 비용은 전체 코드 분석 1/3과 동일한 코드 생성 순서이다. 많은 Annotation Processor의 경우, 이는 Processor 자체에 소요되는 시간보다 훨씬 길다. 40 | 41 | KAPT와 달리, KSP Processor는 자바의 관점에서 입력 프로그램을 보지 못한다. KSP는 KAPT 처럼 javac에 위임하지 않기 때문에 JVM 특정 동작을 가정하지 않으며 잠재적으로 다른 플랫폼에서 사용할 수 있다. 42 | 43 | ### Limitation 44 | 45 | KSP는 대부분의 일반 사용 사례에 대해 간단한 솔루션이 되려고 하지만, 다른 플러그인 솔루에 비해 몇 가지 trade-off가 있다. 다음 사항들은 KSP의 목표가 아니다. 46 | 47 | - 소스코드의 표현 수준 정보를 검토하는 것 48 | - 소스코드를 변경하는 것 49 | - 자바 Annotation Processing API과 100% 호환되는 것 50 | 51 | ### Reference 52 | 53 | - https://github.com/google/ksp 54 | - https://github.com/google/ksp/blob/main/docs/why-ksp.md -------------------------------------------------------------------------------- /006. Android System UI/img/auSbY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeepDive/Study/3b66205e6995ae4ff245d40187991b46edb385d2/006. Android System UI/img/auSbY.png -------------------------------------------------------------------------------- /006. Android System UI/toc.md: -------------------------------------------------------------------------------- 1 | ## Android System UI TOC 2 | 3 | ![](./img/auSbY.png) 4 | 5 | ## Android System UI TOC 6 | 7 | ### Part 1 - Overview (김남훈님, 곽욱현님) 8 | - Status Bar / Navigation Bar 소개 9 | - PDK 딥 다이브 - System UI 리스트 10 | - System UI 실행 프로세스 (Android 11 기준) 11 | 12 | ### Part 2 - Full Screen Case (옥수환님, 이기정님) 13 | - 전체화면 적용시 System ui 적용의 어려움 14 | - 전체화면 종료 후 돌아오는 것 15 | - cutout : https://developer.android.com/guide/topics/display-cutout 16 | - Navigation 영역이 hardware냐 soft냐에 따른 동작 17 | 18 | ### Part 3 - Android api 30 practice (송시영님, 정세희님, 이기정님) 19 | - Android 11 기준으로 insets / compat 관련 신규 기능 출시 대응 -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/007_MVVM V.S MVI Part 1.md: -------------------------------------------------------------------------------- 1 | # MVVM vs MVI Part 1 - What is MVVM Pattern? 2 | 3 | **MVVM** 은 `Model` + `View` + `ViewModel`을 사용하는 아키텍쳐 패턴이다. 4 | 5 | 이러한 구조는 실제 사용자에게 노출되는 화면에서 보이는 영역 즉, **프레젠테이션 레이어** 를 제어하는 것에 그 목적이 있다. 6 | 7 | `View`는 `ViewModel`에 input을 통해 요청을 하고, `ViewModel`은 받은 요청을 핸들링하여 `Model`을 통해 가공된 데이터를 넘겨받는다. 8 | 9 | 이후 가공한 `Model`을 `View`에 반영하는 것이 **MVVM** 패턴의 기본 구조이다. 10 | 11 | 이러한 패턴을 사용하기까지 어떤 방식으로 발전해왔는지 살펴보자. 12 | 13 | ## MVVM의 역사 14 | 15 | MVVM 패턴이 대중화되기 이전엔 MVC와 MVP라는 패턴이 존재하였다. 16 | 17 | GUI가 대중화 되면서 최초에 `Model`, `View`, `Controller`를 분리하려는 시도가 있었으며 **MVC** 라는 패턴으로 고착화되었다. 18 | 19 | ![MVP Architecture](https://imgur.com/4DuJsmi.jpg) 20 | 21 | 이후 **MVC** 의 고질적인 문제인 `View`와 `Model`간의 의존성을 낮추기 위해 `Controller` 대신 `Presenter`라는 개념을 사용하기 시작하였다. 22 | 23 | `Presenter`는 `View`와 `Model`을 1:1 관계로 연결해주는 방식으로 동작하여 `View`와 `Model`사이의 의존성을 끊어내는 역할을 하며 이러한 방식 또한 **MVP** 라는 패턴으로 고착화 되었다. 24 | 25 | ![MVVM Architecture](https://imgur.com/3rbkLy2.jpg) 26 | 27 | 그러나 **MVP**또한 `View`와 `Presenter`사이의 의존성을 여전히 존재하는 문제점이 있었고, `View`와 `ViewModel`, `View`와 `Model` 사이의 의존성을 끊어내기 위해 커맨드 패턴과 데이터 바인딩을 사용하는 **MVVM** 으로 발전해오게 되었다. 28 | 29 | > **참고** [wikipedia#Command pattern](https://en.wikipedia.org/wiki/Command_pattern) 30 | 31 | ### Presentation Model 32 | 33 | 위의 **MVP** 와 **MVVM** 의 흐름도를 보면 굉장히 유사해보이는데, **MVVM** 이 **MVP** 의 개선을 거쳐 완성된 패턴이기 때문이다. 34 | 35 | ![Presentation Model Architecture](https://imgur.com/tiDQQQx.jpg) 36 | 37 | 이때 쓰인 **PM(Presentation Model)** 이라는 개념은 2004년 마틴 파울러에 의해 발표되었고, 해당 모델은 **MVP** 와 굉장히 유사하다. 38 | 39 | > **참고** [Martin Fowler's Presentation Model](https://martinfowler.com/eaaDev/PresentationModel.html) 40 | 41 | ### MVVM 패턴의 고안 42 | 43 | ![The MVVM pattern](https://docs.microsoft.com/en-us/xamarin/xamarin-forms/enterprise-application-patterns/mvvm-images/mvvm.png) 44 | 45 | **MVVM** 패턴의 경우 마이크로소프트의 아키텍트인 켄 쿠퍼와 테드 피터스에 의해 발명되었고, 2005년 존 고스만이 자신의 블로그를 통해 발표되었다. 46 | 47 | 이때 발표된 **MVVM** 패턴은 위에서 언급한 **PM** 패턴의 변형을 통해 만들어졌으며 48 | 49 | 마틴 파울러가 Presentation Model은 View의 추상화에 목적이 있다고 한 반면, 존 고스만은 **MVVM** 을 WPF의 핵심 기능을 활용하여 UI 제작을 간소화하는 방법이라고 소개한다. 50 | 51 | 여기서 **MVVM** 은 View의 상태와 동작을 포함하는 추상화를 사용한다는 점에서 Presentation Model과 동일하다. 52 | 53 | ### MVP vs MVVM 54 | 55 | **MVP** 에서 다루는 Presenter와 달리 **MVVM** 의 ViewModel에서는 View에 대한 참조가 필요없다. 56 | 57 | View는 ViewModel의 속성에 의해 바인딩되며, ViewModel은 Model에 포함되어있는 데이터를 View에 해당하는 다른 State에 연결해준다. 58 | 59 | 이러한 연결 방식을 **Data Binding** 이라고 부른다. 60 | 61 | **MVVM** 을 구현하기 위해 사용되는 요소들에 대해 간단하게 정리해보면 아래와 같다. 62 | 63 | #### 1. View 64 | 사용자가 화면에서 보는 구조와 배치 및 형태 즉, 화면에 표현되는 레이아웃에 대해 관여한다. 65 | 66 | 기본적으로는 비즈니스 로직을 배제하지만, UI와 관련된 로직을 수행할수도 있다. 67 | 68 | 안드로이드에서는 XML을 통한 레이아웃 작성이 기본적으로 해당되며, 69 | 70 | 이를 제어하는 라이프 사이클 컴포넌트인 Activity, Fragment에서 뷰를 인플레이션을 통해 바인딩한다. 71 | 72 | 화면에 Model을 시각적으로 표현하고, 클릭, 키보드 등의 사용자와의 상호작용을 수신하여 바인딩된 ViewModel로 전달하며, 기본적으로 비즈니스 모델과 분리되어 있어야한다. 73 | 74 | #### 2. ViewModel 75 | 76 | ViewModel은 View에 대한 추상화이다. 77 | 78 | View에 연결 할 데이터와 명령으로 구성되어있으며 변경 알림을 통해서 View에게 상태 변화를 전달한다. 79 | 80 | 전달받은 상태변화를 화면에 반영할지는 View 가 결정한다. 81 | 82 | 안드로이드에서는 뒤에서 설명할 AAC ViewModel을 통해 생명주기에 맞게 데이터를 관리하는 역할을 수행한다. 83 | 84 | MVVM에서 추구하는 ViewModel은 MVP와는 다르게 View와 ViewModel이 1:n의 관계를 가질 수 있고, 85 | 86 | 이에 따라 View에는 여러 비즈니스 로직을 여러 ViewModel로 나눌 수 있다. 87 | 88 | 하지만, 안드로이드에서는 AAC ViewModel을 기반으로한 MVVM 아키텍쳐 구현을 하는 경우 1:1을 사용하라는 가이드라인이 명시되어 있다. 89 | 90 | 이는 하나의 ViewModel이 내부적으로 하나의 라이프사이클 컴포넌트에 대한 생명주기를 제어하기 때문이다. 91 | 92 | #### 3. Model 93 | 94 | 좁은 의미로는 Presentationx 레이어에서 상태를 보존하는 레이어로 볼 수 있지만, 95 | 96 | 데이터를 관리하는 DB, 데이터 소스, Repository, UseCase 등 데이터를 가공하는 영역까지를 의미한다. 97 | 98 | MVVM에서는 View는 의존 관계가 없기 때문에 Model을 알지못하고, 직접적인 접근을 하지 않는다. 99 | 100 | ViewModel에서 처리 시 데이터를 구독할 수 있게 처리하고, Model은 이에대한 이벤트를 보내 ViewModel에서 데이터를 관리하게 된다. 101 | 102 | 데이터 이외에 비즈니스 로직을 표현하는 케이스도 해당한다. 103 | 104 | #### 4. Binder 105 | 106 | ViewModel의 속성 값이 변경되면 새 값이 데이터 바인딩을 통해 자동으로 View로 전파된다. 107 | 108 | 예를 들어 사용자가 View안에 있는 버튼을 클릭하게된다면, 해당 요청을 수행하기 위해 ViewModel에 있는 명령이 실행이된다. 109 | 110 | View는 절대로 상태에 대한 변화에 직접적으로 제어하지 않으며, ViewModel이 명령에 대한 값에 대한 변화를 일으키게 된다. 111 | 112 | MVVM에서는 Controller나 Presenter의 역할을 대신하여 Binder가 View의 연결된 속성과 View 사이의 통신을 자동화 한다. 113 | 114 | 이를 통해 알 수 있는 차이점은 기존 MVP의 경우 Presenter가 View에 직접 접근하여 데이터에 대한 변화를 일으켰지만, 115 | 116 | ViewModel은 Binder를 통해 속성에 직접 연결된 채로 업데이트를 주고받을 수 있다. 117 | 118 | 효율적으로 작동하기 위해서는 바인딩을 하기위한 Boilerplate Code의 자동 생성이 필요하게 된다. 119 | 120 | MVVM이 적용된 WPF를 보면, XML 마크업 언어를 통해 데이터 바인딩 기술을 통해 UI에 대한 상태와 ViewModel의 상태를 동기화 한다. 121 | 122 | > **참고** [이종철의 블로그#WPF 데이터바인딩 이란?](https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=leejongcheol2018&logNo=221452069250) -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/007_MVVM V.S MVI Part 3.md: -------------------------------------------------------------------------------- 1 | # MVVM vs MVI Part 3 - What is MVI Architecture Pattern? 2 | 3 | ## MVI의 등장 4 | 5 | **MVVM** 아키텍쳐 패턴에 대해 살펴보았으니, 이번엔 **MVI** 아키텍쳐 패턴에 대해 살펴보자. 6 | 7 | **MVI** 는 3개의 개념을 골자로 한다. 8 | 9 | 1. 단방향 데이터 흐름 (unidirectional cycle of data) 10 | 2. 차단되지않는 의도 (Processing of intents is non-blocking) 11 | 3. 상태의 불변성 (The state is immutable) 12 | 13 | **MVI** 의 구성요소는 `Model`, `View`, `Intent`로, 아래의 흐름대로 동작한다. 14 | 15 | ![MVI Architecture](https://imgur.com/LlKp9DH.jpg) 16 | 17 | **MVVM** 과 비교해보면 흐름도상 양방향 의존성을 가지지않는데, 이러한 특징을 **UDA** 라고 한다. 18 | 19 | UDA에 대해서 먼저 살펴보자. 20 | 21 | ### UDA : UniDirectional Architecture 22 | 23 | **UDA** 는 말 그대로 단방향 구조를 뜻한다. 24 | 25 | 이러한 구조는 javascript 생태계에서 파생된 것인데, 일반적으로 사용자가 예측하는 흐름과 실제 뷰가 지니는 상태가 달라지는 문제를 해결하기 위해 나온 방안이다. 26 | 27 | UDA 구조를 사용하면 View에 영향을 주는 State는 한 방향으로만 수정할 수 있고, 앞의 동작이 끝난 후 다음 동작을 수행하는 동기적인 방식으로 실행된다. 28 | 29 | 이러한 구조를 Android에도 도입하는 것이 **MVI** 아키텍쳐 패턴인 것이다. 30 | 31 | 기존 **MVC** 아키텍쳐 패턴에서 `Controller`가 `View`에 대한 제어를 하던 것과 다르게 `Intent`라는 요소를 사용해서 처리하게 된다. 32 | 33 | **MVC** 아키텍쳐 패턴을 통해 좀 더 자세히 **UDA** 에 대해 이해해보자. 34 | 35 | ### MVC : Model-View-Controller 36 | 37 | 과거 사용되었던 **Smalltalk-80** 이라는 언어가 있다. 38 | 39 | 과거의 **MVC** 는 현재의 **MVC** 와 다르게 **Smalltalk-80** 을 위해 고안된 방식이다. 40 | 41 | 좀 더 자세한 내용은 아래 레퍼런스를 참고하기 바란다. 42 | 43 | > **참고** [Looking at Model-View-Controller in Cocoa](https://www.cocoawithlove.com/blog/mvc-and-cocoa.html) 44 | > **참고** [Interactive Application Architecture Patterns](http://aspiringcraftsman.com/2007/08/25/interactive-application-architecture/) 45 | 46 | 간단하게 그림으로 Smalltalk-80의 MVC를 표현하면 아래와 같다. 47 | 48 | ![Smalltalk-80 MVC](https://imgur.com/2iDilf3.jpg) 49 | 50 | 특징적인 부분은 Model이 애플리케이션의 State와 비즈니스 로직을 관리한다는 것이다. 51 | 52 | View는 Model을 화면에 렌더링해주고, Model의 현재 상태를 Update한다. 53 | 54 | View와 User 사이에서 어떠한 상호작용이 발생하면 이를 `Controller`가 받아서 수행한다. 55 | 56 | 모든 흐름이 단방향으로 흐르고 있는데, 이러한 구조를 UDA라고 이해하면 된다. 57 | 58 | ### MVVM과 MVP의 한계점 59 | 60 | 기존의 **MVP** 와 **MVVM** 도 충분히 좋은 아키텍쳐 패턴이고 널리 쓰이고 있는데, 개발자가 **UDA** 까지 알아야할까? 61 | 62 | **MVVM** 이 **MVP** 의 단점을 극복하기 위해 고안되었듯이, **MVI** 도 **MVVM** 의 단점을 극복하기 위해 고안된 것이다. 63 | 64 | **MVP** 와 **MVVM** 이 공통적으로 가지고 있는 단점은 아래와 같다. 65 | 66 | 1. Presenter, ViewModel은 비즈니스 로직이 언제든지 꼬일 수 있고, 여러가지 State가 공존하기 때문에 View의 State와 Presenter의 State가 서로 다를 수 있다. 67 | 2. User와의 상호작용, 백그라운드의 업데이트 등 다중 스레드 상황에서 Model에 대한 업데이트가 동시에 일어나는 경우 State 관리가 쉽지않다. 68 | 69 | 좀 더 빠른 이해를 위해 두 가지 케이스에 대해 살펴보자. 70 | 71 | #### Case 1. Multiple states in business logic 72 | 73 | 우리가 기존에 사용하던 **MVP**, **MVVM** 에서는 여러 State를 Presenter, ViewModel에서 관리하였다. 74 | 75 | 이때 State 1, 2, 3이 주어지고 이를 연속적으로 반영하게 되거나, 반영 중간에 오류가 발생하는 경우 아래 그림처럼 흐름이 그려진다. 76 | 77 | ![Multiple State Expection](https://imgur.com/oe2gRbX.jpg) 78 | 79 | 이때 사이드 이펙트가 발생하는 경우, 위의 그림의 기대와는 달리 아래 그림처럼 흘러갈 수도 있다. 80 | 81 | ![Multiple States Unexpection](https://imgur.com/v0gN8D4.jpg) 82 | 83 | 만약 1번에서 사이드 이펙트가 발생했다면, 실제 시대와는 달리 X만 보여지게 될 것이다. 84 | 85 | #### Case 2. Asyncronous updates in business logic 86 | 87 | 동일한 상황을 이번엔 비동기 업데이트로 한다고 가정해보자. 88 | 89 | 기대 결과는 아래 그림과 같다. 90 | 91 | ![Asynctronous Updates Expection](https://imgur.com/iPvDyhN.jpg) 92 | 93 | 이때 비동기로 인해 호출된 순서와는 다르게 동작하여 아래 그림과 같이 기대 결과에 맞지않는 결과가 나올 수 있다. 94 | 95 | ![Asynctronous Updates Unexpection](https://imgur.com/ybsUcyn.jpg) 96 | 97 | 2번을 호출하는 과정에서 사이드 이펙트가 발생하여 1초만큼 딜레이가 되는 상황이 발생하게 되었을 때, 98 | 99 | 우리는 기대한 1, 2, 3에 대한 상태를 화면에 반영하는 것을 볼 수 없을 것이다. 100 | 101 | 위의 예시들과 같은 이유들로 인해, 상태의 변경 혹은 반영에 대해 하나의 상태로 관리하는 UDA 구조가 필요함을 알 수 있다. 102 | 103 | UDA에서는 Single State를 통해 상태변경 시 사이드 이펙트가 발생하더라도, 우리가 기대한 화면을 볼 수 있도록 설계할 수 있다. 104 | 105 | ### Immutable State and UDA 106 | 107 | ![Immutable State Flow](https://imgur.com/u5Xx82K.jpg) 108 | 109 | UDA에서는 상태 변화에 대해 단방향으로 처리하여 상태에 대해 사이드 이펙트가 발생하더라도 우리가 기대한 화면을 보여줄 수 있도록 구현할 수 있다. 110 | 111 | UDA는 View와 Model을 직접 연결하는 방식으로 112 | 113 | UDA는 뷰와 모델을 직접 연결하여 위 문제를 해결했다. UDA에서는 모든 상태정보가 모델에서 관리된다. 또한, UDA의 차별화된 특징은 View와 State가 완전히 분리되어 있다. 114 | 115 | UDA를 어떤 방식으로 만들더라도 반드시 지켜야 할 것은 단방향 흐름으로 구조화 해야한다는 것이다. 116 | 117 | ### MVI의 구성 요소 118 | 119 | 상술했듯, MVI 아키텍쳐 패턴은 `Model`, `View`, `Intent`로 구성되어 있다. 120 | 121 | #### 1. Model 122 | 123 | View의 상태를 표현하고, View를 올바르게 렌더링하는 데 필요한 모든 정보가 포함되어 있다. 124 | 125 | Model은 애플리케이션이 현재 가지고 있는 상태정보를 가지고 있으며, 126 | 127 | 전달된 intent(=유저의 의도)를 분석해 현재 상태에 맞추어 새로운 불변 객체인 Model을 생성한다. 128 | 129 | 여기서 상태란 데이터 로딩, 에러, 현재 화면의 포지션, 유저의 의도에 따른 UI 변경 등을 포괄한다. 130 | 131 | 상술한 설명과 같이, MVI에서의 Model은 다른 아키텍쳐 패턴과는 개념이 다르며, 상태를 포괄하고 있다는 점에 주목해야한다. 132 | 133 | 즉, MVI의 `Model-View-Intent`는 `Model(State)-View-Intent`로 표현될 수 있다. 134 | 135 | 아래의 코드가 그 예시이다. 136 | 137 | ```kotlin 138 | data class State( 139 | val isLoading: Boolean, 140 | val error: Throwable, 141 | val persons: List 142 | ) 143 | ``` 144 | 145 | #### 2. View 146 | 147 | View는 사용자의 행동과 시스템 이벤트를 관찰한다. 148 | 149 | View를 통해 트리거된 이벤트에 대한 의도를 설정하며, Model의 상태 변화에 대응한다. 150 | 151 | 이때 Model로부터 상태를 전달받아 화면에 UI를 렌더링한다. 152 | 153 | #### 3. Intent 154 | 155 | 애플리케이션의 상태 즉, Model의 상태를 변경하는 작업을 표현한다. 156 | 157 | 유저의 의도(Action or Event)가 무엇인지 정의하고 있다. 158 | 159 | 결과만 놓고 보자면 Intent를 주입받아 Model을 생성하여 전달하고 이를 View를 통해 표현하게 된다. 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/007_MVVM V.S MVI Part 4.md: -------------------------------------------------------------------------------- 1 | # MVVM vs MVI Part 4 - MVI for Android 2 | 3 | ## Android에서의 MVI 사용 방법 4 | 5 | ### 대표적인 라이브러리들 6 | 7 | Android에도 UDA 구조를 갖는 여러 라이브러리가 존재한다. 8 | 9 | 대표적으로는 Facebook에서 MVC 이슈 해결을 위해 개발한 `Flux` 아키텍쳐 기반의 라이브러리가 있다. 10 | 11 | 이 Flux를 기반으로 구현체는 `Redux`가 나왔는데, `ReduxKotlin` 라이브러리도 별도로 존재한다. 12 | 13 | 그 다음으로는 AirBnb에서 만든 `Mavericks`가 있고 `MVI Mosby`, `MVI Orbit` `Roxie`, `Uniflow-kt` 등의 라이브러리가 존재한다. 14 | 15 | > **참고** 16 | > [Facebook Flux](https://facebook.github.io/flux/) 17 | > [ReduxKotlin](https://reduxkotlin.org/) 18 | > [Airbnb Mavericks](https://airbnb.io/mavericks/) 19 | > [MVI Mosby](https://github.com/sockeqwe/mosby) 20 | > [MVI Orbit](https://orbit-mvi.org/) 21 | > [WW Tech Roxie](https://github.com/ww-tech/roxie) 22 | > [Uniflow](https://github.com/uniflow-kt/uniflow-kt) 23 | 24 | 위 라이브러리들의 기능 비교표는 아래 이미지를 참고하자. 25 | 26 | ![MVI Comparison](https://imgur.com/XHZ7BMA.jpg) 27 | 28 | > **출처** [Matthew Dolan's Medium Top Android MVI libraries in 2021](https://appmattus.medium.com/top-android-mvi-libraries-in-2021-de1afe890f27) 29 | 30 | #### Moxy Library Flow 31 | 32 | Mosby의 파생 라이브러리인 `Moxy`는 UDA 구조로 구현되어있는 MVP 라이브러리이다. 33 | 34 | 이 Moxy 기반 구조에서 View의 상태가 어떻게 변화하는 지 시각화한 자료가 있어 첨부한다. 35 | 36 | ![Mosby Flow](https://imgur.com/bXOAaVt.gif) 37 | 38 | 위의 그림을 보면 Presenter를 사용하고 있음에도, MVI 방식을 구현하는 데 전혀 문제가 없음을 알 수 있다. 39 | 40 | 이를 통해 MVI 아키텍쳐 패턴은 비즈니스 로직을 ViewModel 혹은 Presenter에 위임하여 View의 상태를 관리할 수 있음을 알 수 있다. 41 | 42 | Android 에서는 이러한 방식을 보다 많이 채택하는 추세이며, 최근에는 화면에 대한 설정 변경이 발생하였을 때, 이를 위해 데이터를 보존하는 AAC ViewModel과도 함께 사용되고 있다. 43 | 44 | 45 | ### Android MVI의 동작 흐름 46 | 47 | ![MVI Flow](https://imgur.com/5jkwWxI.jpg) 48 | 49 | 대부분의 Android 라이브러리에서는 위의 흐름도에 따라 동작하고 있다. 50 | 51 | 동작을 순서대로 표기하면 아래와 같다. 52 | 53 | 1. `Intent`로부터 `User`의 입력을 가져온다 54 | 2. `Intent`는 `Model`에서 처리해야하는 동작을 생성한다 55 | 3. `Model`은 `Intent`에서 동작을 가져온다 56 | 4. `Model`은 `Immutable`한 새로운 모델을 생성한다. 57 | 5. `View`는 새로운 `Model`을 가져와 표시한다. 58 | 59 | 위의 그림과 동작 순서를 살펴보면 UDA 구조를 가지고 있는 것을 확인할 수 있다. 60 | 61 | #### Redux 기반 MVI의 동작 흐름 62 | 63 | 그림 대신 텍스트로 MVI의 흐름을 나타내면 아래와 같이 명세할 수 있다. 64 | 65 | `View - Intent - Action - Processor - Result - State - View` 66 | 67 | 1. `View`에서 사용자의 의도인 `Intent`가 전달된다. 68 | 2. `Intent`는 적절한 `Action`으로 변환된다. 69 | 3. `Action`은 `Processor`를 통해 `Result`로 주어지며, 이때 `Processor`가 API 통신 및 데이터베이스 쿼리 등의 사이드 이펙트를 수반한 작업도 수행한다. 70 | 4. 반환된 `Result`는 `Reducer`를 통해 새로운 `State`를 만들어낸다. 이때 만들어진 `State`는 이전의 `State`와 `Result`를 조합하여 만드는 데, 이를 통해 불변성을 유지하게 된다. 71 | 5. 만들어진 `State`가 `View`로 전달되어 사용자에게 렌더링 된다. 72 | 73 | 74 | ## MVI for Android에서의 장점과 단점 75 | 76 | ### 장점 77 | - View와 Model, View와 ViewModel 사이의 의존성이 없으므로 유닛 테스트가 더 용이하다. 78 | - UDA 구조 특성상 단방향이므로 데이터 흐름을 쉽게 추적하고 예측할 수 있다. 79 | - 하나의 불변 상태를 가지고 있기때문에, 상태 간의 충돌을 회피할 수 있다. (=Thread Safety) 80 | - 상태 자체에 초점을 둔 아키텍쳐 패턴으로 타 패턴에 비해 상대적으로 상태를 관리하기 쉽다. 81 | - 타 패턴에 비해 각 컴포넌트간 커플링이 좀 더 약하다. 82 | 83 | 84 | ### 단점 85 | - 모든 상태에 대해 많은 객체를 만들어야 하므로, 메모리 관리 비용이 많이 든다. 86 | - 라이프사이클을 직접 관리해야 한다. 87 | - 작은 상태 변경일지라도 전부 intent로 정의하기때문에 불편함이 유발될 수 있다. 88 | 89 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/007_MVVM V.S MVI Part 5.md: -------------------------------------------------------------------------------- 1 | 2 | ### Part 5 : Compare MVVM with MVI / Conclusion 3 | - Part 2와 Part 4의 예제 비교 4 | - MVVM Practice 5 | - MVI Practice 6 | - Comparison. 7 | - Conclusion 8 | - 개인 생각 정리 / 회고 9 | - 상황 / 케이스 10 | - ... 11 | 12 | 13 | ## References 14 | 15 | > **참고** [wikipedia#Command pattern](https://en.wikipedia.org/wiki/Command_pattern) 16 | > **참고** [Martin Fowler's Presentation Model](https://martinfowler.com/eaaDev/PresentationModel.html) 17 | > **참고** [이종철의 블로그#WPF 데이터바인딩 이란?](https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=leejongcheol2018&logNo=221452069250) 18 | > **출처** [Android Developers#Data Binding Library](https://developer.android.com/topic/libraries/data-binding) 19 | 20 | 21 | [https://ko.wikipedia.org/wiki/%EB%94%94%EC%9E%90%EC%9D%B8_%ED%8C%A8%ED%84%B4#%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99%EC%97%90%EC%84%9C%EC%9D%98_%EB%94%94%EC%9E%90%EC%9D%B8_%ED%8C%A8%ED%84%B4](https://ko.wikipedia.org/wiki/%EB%94%94%EC%9E%90%EC%9D%B8_%ED%8C%A8%ED%84%B4#%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99%EC%97%90%EC%84%9C%EC%9D%98_%EB%94%94%EC%9E%90%EC%9D%B8_%ED%8C%A8%ED%84%B4) 22 | 23 | [https://gmlwjd9405.github.io/2018/07/06/design-pattern.html](https://gmlwjd9405.github.io/2018/07/06/design-pattern.html) 24 | 25 | 26 | [https://ko.wikipedia.org/wiki/%EB%AA%A8%EB%8D%B8-%EB%B7%B0-%EB%B7%B0%EB%AA%A8%EB%8D%B8](https://ko.wikipedia.org/wiki/%EB%AA%A8%EB%8D%B8-%EB%B7%B0-%EB%B7%B0%EB%AA%A8%EB%8D%B8) 27 | 28 | [https://velog.io/@k7120792/Model-View-ViewModel-Pattern](https://velog.io/@k7120792/Model-View-ViewModel-Pattern) 29 | 30 | [https://amsterdamstandard.com/en/post/modern-android-architecture-with-mvi-design-pattern](https://amsterdamstandard.com/en/post/modern-android-architecture-with-mvi-design-pattern) 31 | 32 | [https://dhha22.github.io/android/2018/02/07/Model-View-ViewModel.html](https://dhha22.github.io/android/2018/02/07/Model-View-ViewModel.html) 33 | 34 | [https://ichi.pro/ko/android-mvi-ban-eung-hyeong-akitegcheo-paeteon-128530942790279](https://ichi.pro/ko/android-mvi-ban-eung-hyeong-akitegcheo-paeteon-128530942790279) 35 | 36 | https://docs.microsoft.com/en-us/xamarin/xamarin-forms/enterprise-application-patterns/mvvm 37 | 38 | https://docs.microsoft.com/en-us/xamarin/xamarin-forms/enterprise-application-patterns/mvvm_ 39 | https://www.raywenderlich.com/817602-mvi-architecture-for-android-tutorial-getting-started_ 40 | https://www.ericthecoder.com/2020/07/20/battle-of-the-android-architectures-mvp-vs-mvvm-vs-mvi/_ 41 | 42 | 43 | - https://hannesdorfmann.com/android/mosby3-mvi-1/ 44 | - https://adambennett.dev/2019/07/mvi-the-good-the-bad-and-the-ugly/ 45 | - https://jaehochoe.medium.com/android-mvi-7304bc7e1a84 46 | - https://medium.com/myrealtrip-product/android-mvi-79809c5c14f0 47 | - https://kennethss.medium.com/android-mvi%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90-uda-b16f116c7e34 48 | - https://www.raywenderlich.com/817602-mvi-architecture-for-android-tutorial-getting-started 49 | - https://hannesdorfmann.com/android/mosby3-mvi-1/ 50 | - https://adambennett.dev/2019/07/mvi-the-good-the-bad-and-the-ugly/ 51 | - https://jaehochoe.medium.com/android-mvi-7304bc7e1a84 52 | - https://medium.com/myrealtrip-product/android-mvi-79809c5c14f0 53 | - https://kennethss.medium.com/android-mvi%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90-uda-b16f116c7e34 54 | - https://www.raywenderlich.com/817602-mvi-architecture-for-android-tutorial-getting-started 55 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ART/mvi_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeepDive/Study/3b66205e6995ae4ff245d40187991b46edb385d2/007. MVVM V.S. MVI/ART/mvi_01.png -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | dependencies { 7 | classpath("com.android.tools.build:gradle:7.0.1") 8 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Dependencies.Kotlin.VERSION}") 9 | classpath ("com.google.dagger:hilt-android-gradle-plugin:${Dependencies.Hilt.VERSION}") 10 | } 11 | } 12 | 13 | //task clean(type: Delete) { 14 | // delete rootProject.buildDir 15 | //} 16 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import Dependencies.applyAndroidX 2 | import Dependencies.applyHilt 3 | import Dependencies.applyRetrofit2 4 | import Dependencies.applyTest 5 | 6 | plugins { 7 | id ("com.android.library") 8 | id ("kotlin-android") 9 | id("kotlin-kapt") 10 | } 11 | 12 | android { 13 | compileSdk = Dependencies.COMPILE_SDK 14 | 15 | buildFeatures { 16 | dataBinding = true 17 | } 18 | 19 | defaultConfig { 20 | minSdk = Dependencies.MIN_SDK 21 | targetSdk = Dependencies.TARGET_SDK 22 | 23 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 24 | consumerProguardFiles("consumer-rules.pro") 25 | } 26 | 27 | buildTypes { 28 | release { 29 | isMinifyEnabled = false 30 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 31 | } 32 | } 33 | compileOptions { 34 | sourceCompatibility = JavaVersion.VERSION_11 35 | targetCompatibility = JavaVersion.VERSION_11 36 | } 37 | kotlinOptions { 38 | jvmTarget = JavaVersion.VERSION_11.toString() 39 | } 40 | } 41 | 42 | 43 | 44 | 45 | 46 | dependencies { 47 | implementation (project(":domain")) 48 | 49 | testImplementation(Dependencies.Kotlin.COROUTINE_TEST) 50 | testImplementation(Dependencies.Kotlin.TEST) 51 | 52 | implementation(Dependencies.Google.MATERIAL) 53 | implementation(Dependencies.AndroidX.PAGING_RUNTIME) 54 | applyAndroidX() 55 | applyTest() 56 | applyRetrofit2() 57 | applyHilt() 58 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeepDive/Study/3b66205e6995ae4ff245d40187991b46edb385d2/007. MVVM V.S. MVI/ArchPatternSample/data/consumer-rules.pro -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/src/androidTest/java/com/charlezz/data/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.data 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext(){ 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.charlezz.data.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/src/main/java/com/charlezz/data/flickr/FlickrPagingSource.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.data.flickr 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import com.charlezz.domain.Photo 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | 9 | class FlickrPagingSource( 10 | private val service: FlickrService, 11 | private val keyword: String? 12 | ) : PagingSource() { 13 | 14 | companion object { 15 | private const val FIRST_PAGE = 1 16 | } 17 | 18 | override fun getRefreshKey(state: PagingState): Int? { 19 | return state.anchorPosition?.let { anchorPosition -> 20 | val anchorPage = state.closestPageToPosition(anchorPosition) 21 | anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) 22 | } 23 | } 24 | 25 | override suspend fun load(params: LoadParams): LoadResult { 26 | 27 | val loadSize = params.loadSize 28 | val key = params.key ?: FIRST_PAGE 29 | 30 | return withContext(Dispatchers.IO){ 31 | val result = if (keyword.isNullOrEmpty()) { 32 | service.getRecent(key, loadSize) 33 | } else { 34 | service.search(keyword, key, loadSize) 35 | } 36 | val data = result.photos.photo.map { FlickrPhotoMapper.toModel(it) } 37 | 38 | val prevKey: Int? = if (result.photos.page == 1) { // first page 39 | null 40 | } else { 41 | result.photos.page - 1 42 | } 43 | 44 | val nextKey: Int? = if (result.photos.page < result.photos.pages) { // last page 45 | result.photos.page + 1 46 | } else { 47 | null 48 | } 49 | LoadResult.Page(data, prevKey, nextKey) 50 | } 51 | 52 | } 53 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/src/main/java/com/charlezz/data/flickr/FlickrPhoto.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.data.flickr 2 | 3 | import com.squareup.moshi.Json 4 | 5 | data class FlickrPhoto( 6 | val id:Long, 7 | val owner:String, 8 | val secret:String, 9 | val server:String, 10 | val farm: Int, 11 | val title:String, 12 | val ispublic:Int, 13 | @Json(name = "ispublic") val isPublic:Int, 14 | @Json(name = "isfriend") val isFriend:Int, 15 | @Json(name = "isfamily") val isFamily:Int, 16 | ) -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/src/main/java/com/charlezz/data/flickr/FlickrPhotoMapper.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.data.flickr 2 | 3 | import com.charlezz.domain.Photo 4 | 5 | object FlickrPhotoMapper{ 6 | fun toModel(dto:FlickrPhoto):Photo{ 7 | return Photo( 8 | title = dto.title, 9 | url = "https://live.staticflickr.com/${dto.server}/${dto.id}_${dto.secret}.jpg" 10 | ) 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/src/main/java/com/charlezz/data/flickr/FlickrPhotos.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.data.flickr 2 | 3 | class FlickrPhotos( 4 | val page: Int, 5 | val pages: Int, 6 | val perpage: Int, 7 | val total: Int, 8 | val photo: List, 9 | ) -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/src/main/java/com/charlezz/data/flickr/FlickrPhotosRepository.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.data.flickr 2 | 3 | import androidx.paging.Pager 4 | import androidx.paging.PagingConfig 5 | import androidx.paging.PagingData 6 | import com.charlezz.domain.Photo 7 | import com.charlezz.domain.repository.PhotosRepository 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | class FlickrPhotosRepository(private val service: FlickrService) : PhotosRepository { 11 | override suspend fun search(keyword: String?): Flow> { 12 | return Pager(PagingConfig(30)){ 13 | FlickrPagingSource(service, keyword) 14 | }.flow 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/src/main/java/com/charlezz/data/flickr/FlickrResult.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.data.flickr 2 | 3 | /** 4 | * "photos": { 5 | "page": 1, 6 | "pages": 1154, 7 | "perpage": 100, 8 | "total": 115323, 9 | "photo": [ 10 | { 11 | "id": "51405312918", 12 | "owner": "153499399@N02", 13 | "secret": "b11c94e3d9", 14 | "server": "65535", 15 | "farm": 66, 16 | "title": "Dollar Tree-Spirit Lake, Iowa", 17 | "ispublic": 1, 18 | "isfriend": 0, 19 | "isfamily": 0 20 | }, ... 21 | ] 22 | }, 23 | "stat": "ok" 24 | */ 25 | data class FlickrResult( 26 | val photos:FlickrPhotos, 27 | val stat: String, 28 | ) -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/src/main/java/com/charlezz/data/flickr/FlickrRetrofitModule.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.data.flickr 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import okhttp3.Interceptor 8 | import okhttp3.OkHttpClient 9 | import retrofit2.Retrofit 10 | import retrofit2.converter.moshi.MoshiConverterFactory 11 | import retrofit2.converter.scalars.ScalarsConverterFactory 12 | 13 | 14 | @InstallIn(SingletonComponent::class) 15 | @Module 16 | object FlickrRetrofitModule { 17 | 18 | // TODO: 2021/09/08 API_KEY 숨기기 19 | @Provides 20 | fun providesIntercepter():Interceptor{ 21 | return Interceptor { chain-> 22 | var request = chain.request() 23 | val newUrl = request.url().newBuilder() 24 | .addQueryParameter("api_key", "82373875cee5bbf255dc5a0be0aba815") 25 | .addQueryParameter("format", "json") 26 | .addQueryParameter("nojsoncallback","1") 27 | .build() 28 | 29 | request = request.newBuilder().url(newUrl).build() 30 | chain.proceed(request) 31 | } 32 | } 33 | 34 | @Provides 35 | fun providesOkHttpClient(interceptor: Interceptor):OkHttpClient{ 36 | return OkHttpClient.Builder() 37 | .addInterceptor(interceptor) 38 | .build() 39 | } 40 | 41 | @Provides 42 | fun providesRetrofit(okHttpClient: OkHttpClient):Retrofit{ 43 | return Retrofit.Builder() 44 | .baseUrl("https://www.flickr.com/services/rest/") 45 | .client(okHttpClient) 46 | .addConverterFactory(MoshiConverterFactory.create()) 47 | .addConverterFactory(ScalarsConverterFactory.create()) 48 | .build() 49 | } 50 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/src/main/java/com/charlezz/data/flickr/FlickrService.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.data.flickr 2 | 3 | import retrofit2.Call 4 | import retrofit2.http.GET 5 | import retrofit2.http.Query 6 | 7 | interface FlickrService { 8 | 9 | @GET("?method=flickr.photos.search") 10 | suspend fun search( 11 | @Query("text") keyword: String, 12 | @Query("page") page: Int = 1, 13 | @Query("per_page") perPage: Int = 100 14 | ): FlickrResult 15 | 16 | @GET("?method=flickr.photos.getRecent") 17 | suspend fun getRecent( 18 | @Query("page") page: Int = 1, 19 | @Query("per_page") perPage: Int = 100 20 | ): FlickrResult 21 | 22 | 23 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/src/test/java/com/charlezz/data/FlickrPagingSourceTest.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.data 2 | 3 | import androidx.paging.PagingSource 4 | import com.charlezz.data.fake.FakeFlickrService 5 | import com.charlezz.data.flickr.FlickrPagingSource 6 | import com.charlezz.data.flickr.FlickrPhoto 7 | import com.charlezz.data.flickr.FlickrPhotoMapper 8 | import com.charlezz.domain.Photo 9 | import kotlinx.coroutines.runBlocking 10 | import org.junit.Test 11 | import kotlin.random.Random 12 | import kotlin.test.assertEquals 13 | 14 | class FlickrPagingSourceTest { 15 | 16 | private val fakePhotos = listOf( 17 | createPhoto(), 18 | createPhoto(), 19 | createPhoto(), 20 | ) 21 | private val fakeService = FakeFlickrService().apply { 22 | fakePhotos.forEach{ photo -> addPhoto(photo)} 23 | } 24 | 25 | @Test 26 | fun pagingSourceTest() = runBlocking { 27 | val pagingSource = FlickrPagingSource(fakeService, "") 28 | assertEquals( 29 | expected = PagingSource.LoadResult.Page( 30 | data = listOf(FlickrPhotoMapper.toModel(fakePhotos[0]),FlickrPhotoMapper.toModel(fakePhotos[1])), 31 | prevKey = null, 32 | nextKey = null, 33 | ), 34 | actual = pagingSource.load( 35 | PagingSource.LoadParams.Refresh( 36 | key = null, 37 | loadSize = 2, 38 | placeholdersEnabled = false 39 | ) 40 | ) 41 | ) 42 | } 43 | 44 | private fun createPhoto(): FlickrPhoto { 45 | return FlickrPhoto( 46 | Random.nextLong(), 47 | "owner", 48 | "owner", 49 | "server", 50 | 0, 51 | "title", 52 | 0, 53 | 0, 54 | 0, 55 | 0 56 | ) 57 | 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/src/test/java/com/charlezz/data/FlickrServiceTest.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.data 2 | 3 | import com.charlezz.data.flickr.FlickrRetrofitModule 4 | import com.charlezz.data.flickr.FlickrService 5 | import kotlinx.coroutines.runBlocking 6 | import org.junit.Before 7 | import org.junit.Test 8 | 9 | class FlickrServiceTest { 10 | 11 | lateinit var flickrService: FlickrService 12 | 13 | @Before 14 | fun setup() { 15 | val intercepter = FlickrRetrofitModule.providesIntercepter() 16 | val okHttpClient = FlickrRetrofitModule.providesOkHttpClient(intercepter) 17 | val retrofit = FlickrRetrofitModule.providesRetrofit(okHttpClient) 18 | this.flickrService = retrofit.create(FlickrService::class.java) 19 | } 20 | 21 | @Test 22 | fun getRecentTest() = runBlocking{ 23 | val result = flickrService.getRecent() 24 | assert(result.stat == "ok") 25 | } 26 | 27 | @Test 28 | fun searchTest() = runBlocking{ 29 | val result = flickrService.search("tree") 30 | assert(result.stat == "ok") 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/data/src/test/java/com/charlezz/data/fake/FakeFlickrService.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.data.fake 2 | 3 | import com.charlezz.data.flickr.FlickrPhoto 4 | import com.charlezz.data.flickr.FlickrPhotos 5 | import com.charlezz.data.flickr.FlickrResult 6 | import com.charlezz.data.flickr.FlickrService 7 | import kotlin.math.min 8 | 9 | class FakeFlickrService : FlickrService { 10 | 11 | private val result = ArrayList() 12 | 13 | fun addPhoto(flickrPhoto: FlickrPhoto) { 14 | result.add(flickrPhoto) 15 | } 16 | 17 | override suspend fun search(keyword: String, page: Int, perPage: Int): FlickrResult { 18 | return getPagedList(page, perPage) 19 | } 20 | 21 | override suspend fun getRecent(page: Int, perPage: Int): FlickrResult { 22 | return getPagedList(page, perPage) 23 | } 24 | 25 | private fun getPagedList(page: Int, perpage: Int): FlickrResult { 26 | val fromIndex = 100 * (page - 1) 27 | val toIndex = min(fromIndex + perpage, result.size) 28 | val photos = result.subList(fromIndex, toIndex) 29 | return FlickrResult( 30 | FlickrPhotos( 31 | page = page, 32 | pages = result.size / perpage, 33 | perpage = perpage, 34 | result.size, 35 | photos 36 | ), "ok" 37 | ) 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/domain/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-library") 3 | id("kotlin") 4 | } 5 | 6 | java { 7 | sourceCompatibility = JavaVersion.VERSION_11 8 | targetCompatibility = JavaVersion.VERSION_11 9 | } 10 | 11 | dependencies { 12 | implementation(Dependencies.AndroidX.PAGING_COMMON) 13 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/domain/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeepDive/Study/3b66205e6995ae4ff245d40187991b46edb385d2/007. MVVM V.S. MVI/ArchPatternSample/domain/consumer-rules.pro -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/domain/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/domain/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/domain/src/main/java/com/charlezz/domain/Photo.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.domain 2 | 3 | data class Photo( 4 | val title:String, 5 | val url:String 6 | ) -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/domain/src/main/java/com/charlezz/domain/repository/PhotosRepository.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.domain.repository 2 | 3 | import androidx.paging.PagingData 4 | import com.charlezz.domain.Photo 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface PhotosRepository { 8 | 9 | suspend fun search(keyword: String?): Flow> 10 | 11 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/domain/src/main/java/com/charlezz/domain/usecase/SearchUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.domain.usecase 2 | 3 | import androidx.paging.PagingData 4 | import com.charlezz.domain.Photo 5 | import com.charlezz.domain.repository.PhotosRepository 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | class SearchUseCase(private val photosRepository: PhotosRepository) { 9 | suspend operator fun invoke(keyword:String?):Flow>{ 10 | return photosRepository.search(keyword) 11 | } 12 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/gradle.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeepDive/Study/3b66205e6995ae4ff245d40187991b46edb385d2/007. MVVM V.S. MVI/ArchPatternSample/gradle.properties -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/gradle.properties.kts: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeepDive/Study/3b66205e6995ae4ff245d40187991b46edb385d2/007. MVVM V.S. MVI/ArchPatternSample/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Aug 25 18:28:58 KST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import Dependencies.applyAndroidX 2 | import Dependencies.applyHilt 3 | import Dependencies.applyRetrofit2 4 | import Dependencies.applyTest 5 | 6 | plugins { 7 | id ("com.android.application") 8 | id ("kotlin-android") 9 | id ("kotlin-kapt") 10 | id ("dagger.hilt.android.plugin") 11 | } 12 | 13 | android { 14 | compileSdk = Dependencies.COMPILE_SDK 15 | buildFeatures { 16 | dataBinding = true 17 | } 18 | 19 | defaultConfig { 20 | applicationId = "com.charlezz.mvi" 21 | minSdk = Dependencies.MIN_SDK 22 | targetSdk = Dependencies.TARGET_SDK 23 | versionCode = 1 24 | versionName = "1.0" 25 | 26 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 27 | } 28 | 29 | buildTypes { 30 | getByName("release") { 31 | isMinifyEnabled = false 32 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 33 | } 34 | } 35 | compileOptions { 36 | sourceCompatibility = JavaVersion.VERSION_11 37 | targetCompatibility = JavaVersion.VERSION_11 38 | } 39 | kotlinOptions { 40 | jvmTarget = JavaVersion.VERSION_11.toString() 41 | } 42 | } 43 | 44 | dependencies { 45 | 46 | implementation (project(":domain")) 47 | implementation (project(":data")) 48 | 49 | implementation(Dependencies.Google.MATERIAL) 50 | applyAndroidX() 51 | applyTest() 52 | applyRetrofit2() 53 | applyHilt() 54 | } 55 | kapt { 56 | correctErrorTypes = true 57 | } 58 | hilt { 59 | enableTransformForLocalTests = true 60 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/androidTest/java/com/charlezz/mvi/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.mvi 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.charlezz.architecture", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/java/com/charlezz/mvi/ui/PhotoActivity.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.mvi.ui 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import com.charlezz.mvi.R 6 | 7 | class PhotoActivity : AppCompatActivity() { 8 | 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | setContentView(R.layout.activity_main) 12 | } 13 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/java/com/charlezz/mvi/ui/PhotoViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.mvi.ui 2 | 3 | import android.widget.TextView 4 | 5 | class PhotoViewModel { 6 | 7 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeepDive/Study/3b66205e6995ae4ff245d40187991b46edb385d2/007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeepDive/Study/3b66205e6995ae4ff245d40187991b46edb385d2/007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeepDive/Study/3b66205e6995ae4ff245d40187991b46edb385d2/007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeepDive/Study/3b66205e6995ae4ff245d40187991b46edb385d2/007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeepDive/Study/3b66205e6995ae4ff245d40187991b46edb385d2/007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeepDive/Study/3b66205e6995ae4ff245d40187991b46edb385d2/007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeepDive/Study/3b66205e6995ae4ff245d40187991b46edb385d2/007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeepDive/Study/3b66205e6995ae4ff245d40187991b46edb385d2/007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeepDive/Study/3b66205e6995ae4ff245d40187991b46edb385d2/007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeepDive/Study/3b66205e6995ae4ff245d40187991b46edb385d2/007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ArchPatternSample 3 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvi/src/test/java/com/charlezz/mvi/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.mvi 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvvm/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvvm/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import Dependencies.applyAndroidX 2 | import Dependencies.applyGlide 3 | import Dependencies.applyHilt 4 | import Dependencies.applyRetrofit2 5 | import Dependencies.applyTest 6 | 7 | plugins { 8 | id ("com.android.application") 9 | id ("kotlin-android") 10 | id ("kotlin-kapt") 11 | id ("dagger.hilt.android.plugin") 12 | 13 | } 14 | 15 | android { 16 | compileSdk = Dependencies.COMPILE_SDK 17 | buildFeatures { 18 | dataBinding = true 19 | } 20 | defaultConfig { 21 | applicationId = "com.charlezz.mvvm" 22 | minSdk = Dependencies.MIN_SDK 23 | targetSdk = Dependencies.TARGET_SDK 24 | versionCode = 1 25 | versionName = "1.0" 26 | 27 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 28 | } 29 | 30 | buildTypes { 31 | getByName("release") { 32 | isMinifyEnabled = false 33 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 34 | } 35 | } 36 | compileOptions { 37 | sourceCompatibility = JavaVersion.VERSION_11 38 | targetCompatibility = JavaVersion.VERSION_11 39 | } 40 | kotlinOptions { 41 | jvmTarget = JavaVersion.VERSION_11.toString() 42 | } 43 | } 44 | 45 | dependencies { 46 | 47 | implementation (project(":domain")) 48 | implementation (project(":data")) 49 | 50 | implementation(Dependencies.Google.MATERIAL) 51 | applyAndroidX() 52 | applyTest() 53 | applyRetrofit2() 54 | applyHilt() 55 | applyGlide() 56 | } 57 | kapt { 58 | correctErrorTypes = true 59 | } 60 | hilt { 61 | enableTransformForLocalTests = true 62 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvvm/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvvm/src/androidTest/java/com/charlezz/mvvm/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.mvvm 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.charlezz.mvvm", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvvm/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvvm/src/main/java/com/charlezz/mvvm/App.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.mvvm 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class App : Application() -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvvm/src/main/java/com/charlezz/mvvm/di/AppModules.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.mvvm.di 2 | 3 | import com.charlezz.data.flickr.FlickrPhotosRepository 4 | import com.charlezz.data.flickr.FlickrService 5 | import com.charlezz.domain.repository.PhotosRepository 6 | import com.charlezz.domain.usecase.SearchUseCase 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | import retrofit2.Retrofit 12 | 13 | @InstallIn(SingletonComponent::class) 14 | @Module 15 | class AppModule { 16 | 17 | @Provides 18 | fun provideFlickrService(retrofit:Retrofit): FlickrService{ 19 | return retrofit.create(FlickrService::class.java) 20 | } 21 | 22 | @Provides 23 | fun providePhotosRepository(flickrService: FlickrService):PhotosRepository{ 24 | return FlickrPhotosRepository(flickrService) 25 | } 26 | 27 | @Provides 28 | fun provideSearchUseCase(photosRepository: PhotosRepository):SearchUseCase{ 29 | return SearchUseCase(photosRepository) 30 | } 31 | 32 | } 33 | 34 | 35 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvvm/src/main/java/com/charlezz/mvvm/ui/BindableViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.mvvm.ui 2 | 3 | import androidx.databinding.ViewDataBinding 4 | import androidx.recyclerview.widget.RecyclerView 5 | 6 | class BindableViewHolder(val binding: T) : 7 | RecyclerView.ViewHolder(binding.root) { 8 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvvm/src/main/java/com/charlezz/mvvm/ui/PhotoActivity.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.mvvm.ui 2 | 3 | import android.content.res.Configuration 4 | import android.os.Bundle 5 | import androidx.activity.viewModels 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.lifecycle.lifecycleScope 9 | import androidx.recyclerview.widget.GridLayoutManager 10 | import com.charlezz.mvvm.R 11 | import com.charlezz.mvvm.databinding.ActivityPhotoBinding 12 | import dagger.hilt.android.AndroidEntryPoint 13 | import kotlinx.coroutines.flow.collectLatest 14 | import kotlinx.coroutines.launch 15 | import android.util.TypedValue 16 | import androidx.core.view.isVisible 17 | import androidx.paging.LoadState 18 | 19 | @AndroidEntryPoint 20 | class PhotoActivity : AppCompatActivity() { 21 | 22 | private val viewModel: PhotoViewModel by viewModels() 23 | 24 | private val adapter = PhotoAdapter() 25 | 26 | private val layoutManager: GridLayoutManager by lazy { 27 | GridLayoutManager(this, calculateSpanCount()) 28 | } 29 | 30 | private val binding: ActivityPhotoBinding by lazy { 31 | DataBindingUtil.setContentView(this, R.layout.activity_photo) 32 | } 33 | 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | super.onCreate(savedInstanceState) 36 | initUI() 37 | } 38 | 39 | private fun initUI() { 40 | binding.lifecycleOwner = this 41 | binding.recyclerView.adapter = adapter 42 | binding.recyclerView.layoutManager = layoutManager 43 | 44 | binding.search.setOnClickListener { 45 | viewModel.load(binding.editText.text.toString()) 46 | } 47 | 48 | adapter.addLoadStateListener { loadStates -> 49 | binding.progressBar.isVisible = loadStates.refresh is LoadState.Loading 50 | } 51 | } 52 | 53 | override fun onPostCreate(savedInstanceState: Bundle?) { 54 | super.onPostCreate(savedInstanceState) 55 | observeViewModel() 56 | } 57 | 58 | private fun observeViewModel() { 59 | lifecycleScope.launch { 60 | viewModel.items.collectLatest { 61 | adapter.submitData(it) 62 | } 63 | } 64 | } 65 | 66 | override fun onConfigurationChanged(newConfig: Configuration) { 67 | super.onConfigurationChanged(newConfig) 68 | layoutManager.spanCount = calculateSpanCount() 69 | } 70 | 71 | private fun calculateSpanCount(): Int { 72 | return resources.displayMetrics.widthPixels / TypedValue.applyDimension( 73 | TypedValue.COMPLEX_UNIT_DIP, 74 | 105.0f, 75 | resources.displayMetrics 76 | ).toInt() 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvvm/src/main/java/com/charlezz/mvvm/ui/PhotoAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.mvvm.ui 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.databinding.DataBindingUtil 6 | import androidx.paging.PagingDataAdapter 7 | import androidx.recyclerview.widget.DiffUtil 8 | import com.charlezz.mvvm.BR 9 | import com.charlezz.mvvm.R 10 | 11 | class PhotoAdapter : PagingDataAdapter>(diffCallback) { 12 | 13 | companion object { 14 | private val diffCallback = object : DiffUtil.ItemCallback() { 15 | override fun areItemsTheSame( 16 | oldItem: PhotoUiModel, 17 | newItem: PhotoUiModel 18 | ): Boolean { 19 | return oldItem.getImageUrl() == newItem.getImageUrl() 20 | } 21 | 22 | override fun areContentsTheSame( 23 | oldItem: PhotoUiModel, 24 | newItem: PhotoUiModel 25 | ): Boolean { 26 | return true 27 | } 28 | } 29 | } 30 | 31 | override fun getItemViewType(position: Int): Int { 32 | return getItem(position)?.let { it.getLayout() } ?: R.layout.view_photo 33 | } 34 | 35 | override fun onBindViewHolder(holder: BindableViewHolder<*>, position: Int) { 36 | getItem(position)?.let { 37 | holder.binding.setVariable(BR.model, it) 38 | holder.binding.executePendingBindings() 39 | } 40 | 41 | } 42 | 43 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindableViewHolder<*> { 44 | return BindableViewHolder( 45 | DataBindingUtil.inflate( 46 | LayoutInflater.from(parent.context), 47 | viewType, 48 | parent, 49 | false 50 | ) 51 | ) 52 | } 53 | 54 | 55 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvvm/src/main/java/com/charlezz/mvvm/ui/PhotoUiModel.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.mvvm.ui 2 | 3 | import com.charlezz.domain.Photo 4 | import com.charlezz.mvvm.R 5 | 6 | class PhotoUiModel(private val photo: Photo) { 7 | 8 | fun getLayout(): Int = R.layout.view_photo 9 | 10 | fun getImageUrl(): String { 11 | return photo.url 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvvm/src/main/java/com/charlezz/mvvm/ui/PhotoViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.mvvm.ui 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import androidx.paging.PagingData 6 | import androidx.paging.cachedIn 7 | import androidx.paging.map 8 | import com.charlezz.domain.usecase.SearchUseCase 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.channels.Channel 11 | import kotlinx.coroutines.flow.* 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class PhotoViewModel @Inject constructor( 16 | private val searchUseCase: SearchUseCase, 17 | ) : ViewModel() { 18 | 19 | private val keyword: MutableStateFlow = MutableStateFlow("") 20 | 21 | val items: Flow> = flowOf( 22 | keyword.asStateFlow().map { PagingData.empty() }, 23 | keyword 24 | .flatMapLatest { keyword -> searchUseCase(keyword) } 25 | .map { pagingData -> pagingData.map { PhotoUiModel(it) } } 26 | .cachedIn(viewModelScope) 27 | ).flattenMerge(2) 28 | 29 | fun load(keyword:String){ 30 | this.keyword.value = keyword 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvvm/src/main/java/com/charlezz/mvvm/util/ImageViewBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.charlezz.mvvm.util 2 | 3 | import android.widget.ImageView 4 | import androidx.databinding.BindingAdapter 5 | import com.bumptech.glide.Glide 6 | 7 | 8 | @BindingAdapter("imageUrl") 9 | fun setImageUrl(view: ImageView, url:String?){ 10 | Glide.with(view).load(url).into(view) 11 | } -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvvm/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /007. MVVM V.S. MVI/ArchPatternSample/mvvm/src/main/res/layout/activity_photo.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 21 | 22 | 27 | 28 | 29 | 30 |