├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── tech
│ │ │ └── thdev
│ │ │ └── app
│ │ │ ├── MainActivity.kt
│ │ │ └── ui
│ │ │ ├── JavaJava.java
│ │ │ ├── MainScreen.kt
│ │ │ ├── SampleScreen.kt
│ │ │ ├── first
│ │ │ ├── FirstFragment.kt
│ │ │ └── FirstViewModel.kt
│ │ │ ├── holder
│ │ │ ├── other
│ │ │ │ └── OtherSampleScreen.kt
│ │ │ └── web
│ │ │ │ ├── LocalWebOwner.kt
│ │ │ │ └── WebSampleScreen.kt
│ │ │ ├── second
│ │ │ ├── SecondFragment.kt
│ │ │ └── SecondViewModel.kt
│ │ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ ├── content_main.xml
│ │ ├── first_fragment.xml
│ │ ├── main_activity.xml
│ │ └── second_fragment.xml
│ │ ├── menu
│ │ └── menu_main.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── navigation
│ │ └── nav_graph.xml
│ │ ├── values-night
│ │ └── themes.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ └── test
│ └── java
│ └── tech
│ └── thdev
│ └── app
│ ├── Counter.kt
│ ├── ExampleUnitTest.kt
│ └── ui
│ └── first
│ ├── FirstViewModelTest.kt
│ └── MutableStateF.kt
├── architecture-one
├── README.md
├── app
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── tech
│ │ │ └── thdev
│ │ │ └── architecture
│ │ │ └── app
│ │ │ ├── ArchitectureOneApplication.kt
│ │ │ ├── common
│ │ │ ├── alert
│ │ │ │ ├── AlertDialogScreen.kt
│ │ │ │ ├── AlertDialogViewModel.kt
│ │ │ │ └── model
│ │ │ │ │ └── AlertDialogUiState.kt
│ │ │ └── snackbar
│ │ │ │ ├── SnackbarScreen.kt
│ │ │ │ ├── SnackbarViewModel.kt
│ │ │ │ └── model
│ │ │ │ └── SnackbarUiState.kt
│ │ │ ├── event
│ │ │ ├── FlowComposeInteractionTrigger.kt
│ │ │ ├── FlowInteraction.kt
│ │ │ ├── FlowInteractionStream.kt
│ │ │ ├── LocalComposeEventTriggerOwner.kt
│ │ │ ├── MainEffect.kt
│ │ │ └── di
│ │ │ │ └── EventModule.kt
│ │ │ ├── feature
│ │ │ ├── MainActivity.kt
│ │ │ ├── MainViewModel.kt
│ │ │ └── model
│ │ │ │ └── MainUiState.kt
│ │ │ └── ui
│ │ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── baseline_close_24.xml
│ │ ├── baseline_home_24.xml
│ │ ├── baseline_web_24.xml
│ │ └── ic_launcher_background.xml
│ │ ├── menu
│ │ └── menu_main.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── values-night
│ │ └── themes.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── themes.xml
├── core-data
│ └── repository
│ │ ├── alert-repository-api
│ │ ├── README.md
│ │ ├── build.gradle.kts
│ │ └── src
│ │ │ └── main
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java
│ │ │ └── tech
│ │ │ └── thdev
│ │ │ └── architecture
│ │ │ └── app
│ │ │ └── alert
│ │ │ └── repository
│ │ │ └── api
│ │ │ ├── AlertRepository.kt
│ │ │ ├── InternalAlertRepository.kt
│ │ │ └── model
│ │ │ ├── AlertEndEvent.kt
│ │ │ └── AlertItem.kt
│ │ ├── alert-repository
│ │ ├── README.md
│ │ ├── build.gradle.kts
│ │ └── src
│ │ │ ├── main
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java
│ │ │ │ └── tech
│ │ │ │ └── thdev
│ │ │ │ └── architecture
│ │ │ │ └── app
│ │ │ │ └── alert
│ │ │ │ └── repository
│ │ │ │ ├── AlertRepositoryImpl.kt
│ │ │ │ └── di
│ │ │ │ └── AlertRepositoryModel.kt
│ │ │ └── test
│ │ │ └── java
│ │ │ └── tech
│ │ │ └── thdev
│ │ │ └── app
│ │ │ └── ExampleUnitTest.kt
│ │ ├── snackbar-repository-api
│ │ ├── README.md
│ │ ├── build.gradle.kts
│ │ └── src
│ │ │ └── main
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java
│ │ │ └── tech
│ │ │ └── thdev
│ │ │ └── architecture
│ │ │ └── app
│ │ │ └── snackbar
│ │ │ └── repository
│ │ │ └── api
│ │ │ ├── InternalSnackbarRepository.kt
│ │ │ ├── SnackbarRepository.kt
│ │ │ └── model
│ │ │ ├── SnackbarEndEvent.kt
│ │ │ └── SnackbarItem.kt
│ │ └── snackbar-repository
│ │ ├── README.md
│ │ ├── build.gradle.kts
│ │ └── src
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── tech
│ │ │ └── thdev
│ │ │ └── architecture
│ │ │ └── app
│ │ │ └── snackbar
│ │ │ └── repository
│ │ │ ├── SnackbarRepositoryImpl.kt
│ │ │ └── di
│ │ │ └── SnackbarRepositoryModel.kt
│ │ └── test
│ │ └── java
│ │ └── tech
│ │ └── thdev
│ │ └── app
│ │ └── ExampleUnitTest.kt
└── images
│ ├── sample_01.png
│ ├── sample_02.png
│ └── sample_03.png
├── build-logic
├── README.md
├── convention
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── kotlin
│ │ ├── AndroidApplicationConventionPlugin.kt
│ │ ├── AndroidLibraryComposeConventionPlugin.kt
│ │ ├── AndroidLibraryComposeFeatureConventionPlugin.kt
│ │ ├── AndroidLibraryConventionPlugin.kt
│ │ ├── AndroidLibraryHiltConventionPlugin.kt
│ │ ├── AndroidLibraryRobolectricConventionPlugin.kt
│ │ ├── AndroidLibraryUnitTestConventionPlugin.kt
│ │ ├── KotlinLibraryConventionPlugin.kt
│ │ ├── KotlinLibraryHiltConventionPlugin.kt
│ │ ├── KotlinLibraryKspConventionPlugin.kt
│ │ ├── KotlinLibrarySerializationConventionPlugin.kt
│ │ ├── KotlinLibraryVerifyDetektConventionPlugin.kt
│ │ ├── tech.thdev.kotlin.library.verify.test.gradle.kts
│ │ └── tech
│ │ └── thdev
│ │ └── gradle
│ │ ├── ComposeAndroid.kt
│ │ ├── CoroutineAndroid.kt
│ │ ├── DaggerAndroid.kt
│ │ ├── KotlinAndroid.kt
│ │ ├── KotlinKsp.kt
│ │ ├── UnitTest.kt
│ │ ├── VerifyDetekt.kt
│ │ ├── VerifyJacoco.kt
│ │ └── extensions
│ │ ├── AppExtension.kt
│ │ ├── ImportExtension.kt
│ │ ├── InternalProjectExtension.kt
│ │ └── InternalVersionExtension.kt
├── gradle.properties
└── settings.gradle.kts
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── sample
├── compose
│ ├── compose-example-01
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── build.gradle.kts
│ │ ├── image
│ │ │ ├── main.png
│ │ │ └── web.png
│ │ ├── proguard-rules.pro
│ │ └── src
│ │ │ ├── main
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java
│ │ │ │ └── tech
│ │ │ │ │ └── thdev
│ │ │ │ │ └── compose
│ │ │ │ │ └── web
│ │ │ │ │ └── sample
│ │ │ │ │ ├── MainActivity.kt
│ │ │ │ │ ├── MainActivityBackup.kt
│ │ │ │ │ └── ui
│ │ │ │ │ ├── design
│ │ │ │ │ └── system
│ │ │ │ │ │ └── button
│ │ │ │ │ │ ├── ExampleButtonComponent.kt
│ │ │ │ │ │ └── ExampleButtonDefaults.kt
│ │ │ │ │ ├── holder
│ │ │ │ │ ├── home
│ │ │ │ │ │ ├── HomeScreenOne.kt
│ │ │ │ │ │ ├── HomeScreenThree.kt
│ │ │ │ │ │ ├── HomeScreenTwo.kt
│ │ │ │ │ │ └── component
│ │ │ │ │ │ │ ├── HomeItemEditComponent.kt
│ │ │ │ │ │ │ ├── HomeItemViewComponent.kt
│ │ │ │ │ │ │ ├── TwoHomeItemEditComponent.kt
│ │ │ │ │ │ │ └── TwoHomeItemViewComponent.kt
│ │ │ │ │ └── web
│ │ │ │ │ │ ├── CustomWebChromeClient.kt
│ │ │ │ │ │ ├── CustomWebViewClient.kt
│ │ │ │ │ │ ├── LocalWebOwner.kt
│ │ │ │ │ │ └── WebScreen.kt
│ │ │ │ │ ├── model
│ │ │ │ │ ├── ListItem.kt
│ │ │ │ │ └── NavigationSample.kt
│ │ │ │ │ └── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Theme.kt
│ │ │ │ │ └── Type.kt
│ │ │ └── res
│ │ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ │ ├── drawable
│ │ │ │ ├── baseline_close_24.xml
│ │ │ │ ├── baseline_home_24.xml
│ │ │ │ ├── baseline_web_24.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── values-night
│ │ │ │ └── themes.xml
│ │ │ │ └── values
│ │ │ │ ├── colors.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── themes.xml
│ │ │ └── test
│ │ │ └── java
│ │ │ └── tech
│ │ │ └── thdev
│ │ │ └── app
│ │ │ └── ExampleUnitTest.kt
│ └── web-sample-01
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── build.gradle.kts
│ │ ├── image
│ │ ├── after.png
│ │ └── before.png
│ │ ├── proguard-rules.pro
│ │ └── src
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── tech
│ │ │ │ └── thdev
│ │ │ │ └── compose
│ │ │ │ └── web
│ │ │ │ └── sample
│ │ │ │ ├── MainActivity.kt
│ │ │ │ └── ui
│ │ │ │ ├── MainScreen.kt
│ │ │ │ ├── holder
│ │ │ │ ├── other
│ │ │ │ │ └── OtherSampleScreen.kt
│ │ │ │ └── web
│ │ │ │ │ ├── LocalWebOwner.kt
│ │ │ │ │ └── WebSampleScreen.kt
│ │ │ │ └── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ └── res
│ │ │ ├── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── drawable
│ │ │ └── ic_launcher_background.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── values-night
│ │ │ └── themes.xml
│ │ │ └── values
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ ├── strings.xml
│ │ │ └── themes.xml
│ │ └── test
│ │ └── java
│ │ └── tech
│ │ └── thdev
│ │ └── app
│ │ └── ExampleUnitTest.kt
└── view
│ └── recycler-view-ex
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ ├── images
│ ├── after.png
│ └── before.png
│ ├── proguard-rules.pro
│ └── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── tech
│ │ │ └── thdev
│ │ │ └── view
│ │ │ └── recycler
│ │ │ └── view
│ │ │ └── ex
│ │ │ ├── MainActivity.kt
│ │ │ ├── MainViewModel.kt
│ │ │ ├── adapter
│ │ │ ├── MainAdapter.kt
│ │ │ ├── MainTrashViewHolder.kt
│ │ │ └── MainViewHolder.kt
│ │ │ └── model
│ │ │ └── SampleItem.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── baseline_perm_media_24.xml
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ ├── item_view.xml
│ │ ├── main_activity.xml
│ │ └── trash_item_view.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── values-night
│ │ └── themes.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ └── test
│ └── java
│ └── tech
│ └── thdev
│ └── app
│ ├── Counter.kt
│ ├── ExampleUnitTest.kt
│ └── ui
│ └── first
│ ├── FirstViewModelTest.kt
│ └── MutableStateF.kt
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Android template
3 | # Built application files
4 | *.apk
5 | *.ap_
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 |
18 | # Gradle files
19 | .gradle/
20 | build/
21 |
22 | # Local configuration file (sdk path, etc)
23 | local.properties
24 |
25 | # Proguard folder generated by Eclipse
26 | proguard/
27 |
28 | # Log Files
29 | *.log
30 |
31 | # Android Studio Navigation editor temp files
32 | .navigation/
33 |
34 | # Android Studio captures folder
35 | captures/
36 |
37 | # Intellij
38 | *.iml
39 | .idea/
40 |
41 | # Keystore files
42 | *.jks
43 |
44 | .DS_Store
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Summary
2 | 블로그에 게시되는 안드로이드 예제를 정리합니다.
3 | 일부 예제는 별도의 Repository를 가질 수 있으며 해당 부분은 아래 링크를 참고해주세요.
4 |
5 |
6 | # Blog
7 | 관련 포스트는 다음의 블로그를 통해 확인 할 수 있습니다.
8 | - [블로그 링크 : thdev.tech](http://thdev.tech)
9 |
10 |
11 | # Etc
12 | 그외 다른 예제의 링크는 아래와 같습니다.
13 | - [MAppPermission](https://github.com/taehwandev/MAppPermission)
14 | - [MediaProjectionExample](https://github.com/taehwandev/MediaProjectionExample)
15 | - [MediaCodecExample](https://github.com/taehwandev/MediaCodecExample)
16 |
17 |
18 | ## Build info
19 |
20 | - Android studio 3.3+
21 |
22 | - compile sdk version = 28
23 | - Target sdk version = 28
24 | - Min sdk version = Depending on the module.
25 |
26 |
27 | ## Library version info
28 |
29 | - Dependencies
30 | - Android Support
31 | - [Support library legacy](https://developer.android.com/topic/libraries/support-library/revisions)
32 | - [Jetpack AndroidX](https://developer.android.com/jetpack/androidx)
33 | - [Jetpack architecture](https://developer.android.com/topic/libraries/architecture/adding-components)
34 |
35 | - UI
36 | - [constraint layout](https://developer.android.com/training/constraint-layout/)
37 |
38 | - ImageLoader
39 | - [Glide](https://github.com/bumptech/glide)
40 |
41 | - Network
42 | - [Retrofit](https://square.github.io/retrofit/)
43 | - [OkHttp](https://github.com/square/okhttp)
44 |
45 | - ReactiveX
46 | - [RxJava](https://github.com/ReactiveX/RxJava)
47 | - [RxAndroid](https://github.com/ReactiveX/RxAndroid)
48 | - [RxKotlin](https://github.com/ReactiveX/RxKotlin)
49 |
50 | - Kotlin
51 | - [kotlin](https://github.com/JetBrains/kotlin)
52 | - [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)
53 |
54 | - thdev support library
55 | - [LifecycleExtensions](https://thdev.tech/LifecycleExtensions/)
56 | - [CoroutinesUIExtensions](https://thdev.tech/CoroutinesUIExtensions/)
57 |
58 |
59 | ## License
60 |
61 | ```
62 | Copyright 2016 Tae-hwan
63 |
64 | Licensed under the Apache License, Version 2.0 (the "License");
65 | you may not use this file except in compliance with the License.
66 | You may obtain a copy of the License at
67 |
68 | http://www.apache.org/licenses/LICENSE-2.0
69 |
70 | Unless required by applicable law or agreed to in writing, software
71 | distributed under the License is distributed on an "AS IS" BASIS,
72 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
73 | See the License for the specific language governing permissions and
74 | limitations under the License.
75 | ```
76 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.application)
3 | }
4 |
5 | android {
6 | val (majorVersion, minorVersion, patchVersion, code) = getVersionInfo()
7 |
8 | defaultConfig {
9 | applicationId = "tech.thdev.app"
10 | minSdk = libs.versions.minSdk.get().toInt()
11 | targetSdk = libs.versions.targetSdk.get().toInt()
12 | vectorDrawables.useSupportLibrary = true
13 | versionCode = code
14 | versionName = "$majorVersion.$minorVersion.$patchVersion"
15 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
16 | multiDexEnabled = true
17 | }
18 |
19 | buildFeatures {
20 | viewBinding = true
21 | dataBinding = true
22 | }
23 | }
24 |
25 | setNamespace("app")
26 |
27 | ksp {
28 | arg("moduleName", project.name)
29 | arg("rootDir", rootDir.absolutePath)
30 | }
31 |
32 | dependencies {
33 | implementation(libs.kotlin.stdlib)
34 |
35 | implementation(libs.google.material)
36 |
37 | implementation(libs.androidx.core)
38 | implementation(libs.androidx.appCompat)
39 | implementation(libs.androidx.activity)
40 | implementation(libs.androidx.constraintLayout)
41 | implementation(libs.androidx.vectorDrawable)
42 | implementation(libs.androidx.navigationFragment)
43 | implementation(libs.androidx.navigationUi)
44 | implementation(libs.androidx.liveData)
45 |
46 | implementation(libs.image.coil)
47 |
48 | implementation(libs.network.retrofit)
49 | implementation(libs.network.okhttp)
50 | implementation(libs.network.okhttp.logging)
51 |
52 | implementation(libs.compose.navigation)
53 |
54 | testImplementation(libs.test.coroutines)
55 | }
--------------------------------------------------------------------------------
/app/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
22 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/thdev/app/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app
2 |
3 | import android.os.Bundle
4 | import android.view.Menu
5 | import android.view.MenuItem
6 | import androidx.activity.compose.setContent
7 | import androidx.appcompat.app.AppCompatActivity
8 | import tech.thdev.app.databinding.MainActivityBinding
9 | import tech.thdev.app.ui.SampleScreen
10 |
11 | class MainActivity : AppCompatActivity() {
12 |
13 | private lateinit var binding: MainActivityBinding
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | setContent {
17 | SampleScreen()
18 | }
19 | // binding = MainActivityBinding.inflate(layoutInflater)
20 | // setContentView(binding.root)
21 | // setSupportActionBar(binding.toolbar)
22 | }
23 |
24 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
25 | // Inflate the menu; this adds items to the action bar if it is present.
26 | menuInflater.inflate(R.menu.menu_main, menu)
27 | return true
28 | }
29 |
30 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
31 | // Handle action bar item clicks here. The action bar will
32 | // automatically handle clicks on the Home/Up button, so long
33 | // as you specify a parent activity in AndroidManifest.xml.
34 | return when (item.itemId) {
35 | R.id.action_settings -> true
36 | else -> super.onOptionsItemSelected(item)
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/thdev/app/ui/JavaJava.java:
--------------------------------------------------------------------------------
1 | package tech.thdev.app.ui;
2 |
3 | public class JavaJava {
4 |
5 | private static JavaJava INSTANCE = new JavaJava();
6 |
7 | public static final String value = "avalue";
8 |
9 | private JavaJava() {
10 | System.out.println("init");
11 | }
12 |
13 | public static String getValue() {
14 | return value;
15 | }
16 |
17 | public static JavaJava getInstance() {
18 | return INSTANCE;
19 | }
20 |
21 | static {
22 | System.out.println("static");
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/thdev/app/ui/first/FirstFragment.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app.ui.first
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.fragment.app.Fragment
8 | import androidx.fragment.app.viewModels
9 | import androidx.lifecycle.Lifecycle
10 | import androidx.lifecycle.lifecycleScope
11 | import androidx.lifecycle.repeatOnLifecycle
12 | import androidx.navigation.fragment.findNavController
13 | import kotlinx.coroutines.flow.collectLatest
14 | import kotlinx.coroutines.launch
15 | import tech.thdev.app.R
16 | import tech.thdev.app.databinding.FirstFragmentBinding
17 |
18 | class FirstFragment : Fragment() {
19 |
20 | private lateinit var binding: FirstFragmentBinding
21 |
22 | private val viewModel: FirstViewModel by viewModels()
23 |
24 | override fun onCreateView(
25 | inflater: LayoutInflater, container: ViewGroup?,
26 | savedInstanceState: Bundle?
27 | ): View {
28 | return FirstFragmentBinding.inflate(inflater).also {
29 | binding = it
30 | }.root
31 | }
32 |
33 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
34 | super.onViewCreated(view, savedInstanceState)
35 |
36 | viewLifecycleOwner.lifecycleScope.launch {
37 | viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
38 | viewModel.state.collectLatest {
39 | // todo
40 | }
41 | }
42 | }
43 |
44 | binding.buttonFirst.setOnClickListener {
45 | findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/thdev/app/ui/first/FirstViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app.ui.first
2 |
3 | import androidx.annotation.VisibleForTesting
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.asStateFlow
8 | import kotlinx.coroutines.launch
9 |
10 | class FirstViewModel(
11 | private val repository: Repository,
12 | ) : ViewModel() {
13 |
14 | @VisibleForTesting
15 | val _state = MutableStateFlow(UiState(data = ""))
16 | val state = _state.asStateFlow()
17 |
18 | fun loadData() {
19 | viewModelScope.launch {
20 | kotlin.runCatching {
21 | repository.getData()
22 | }
23 | .onFailure {
24 | _state.value = UiState(
25 | data = "Error",
26 | )
27 | }
28 | .onSuccess {
29 | _state.value = UiState(
30 | data = it.plus(100).toString(),
31 | )
32 | }
33 | }
34 | }
35 | }
36 |
37 | data class UiState(
38 | val data: String,
39 | )
40 |
41 | interface Repository {
42 |
43 | suspend fun getData(): Int
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/tech/thdev/app/ui/holder/other/OtherSampleScreen.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app.ui.holder.other
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Alignment
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.tooling.preview.Preview
10 |
11 | @Composable
12 | internal fun OtherSampleScreen() {
13 | Box(
14 | contentAlignment = Alignment.Center,
15 | modifier = Modifier
16 | .fillMaxSize()
17 | ) {
18 | Text(
19 | text = "other screen",
20 | )
21 | }
22 | }
23 |
24 | @Preview(
25 | backgroundColor = 0xFFFFFF,
26 | )
27 | @Composable
28 | private fun PreviewOtherSampleScreen() {
29 | OtherSampleScreen()
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/tech/thdev/app/ui/holder/web/LocalWebOwner.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app.ui.holder.web
2 |
3 | import android.webkit.WebView
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.ProvidedValue
6 | import androidx.compose.runtime.staticCompositionLocalOf
7 |
8 | internal object LocalWebOwner {
9 |
10 | private val LocalComposition = staticCompositionLocalOf { null }
11 |
12 | val current: WebView?
13 | @Composable
14 | get() = LocalComposition.current
15 |
16 | infix fun provides(registerOwner: WebView?): ProvidedValue =
17 | LocalComposition provides registerOwner
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/thdev/app/ui/second/SecondFragment.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app.ui.second
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.fragment.app.Fragment
8 | import androidx.fragment.app.viewModels
9 | import androidx.navigation.fragment.findNavController
10 | import tech.thdev.app.R
11 | import tech.thdev.app.databinding.SecondFragmentBinding
12 |
13 | class SecondFragment : Fragment() {
14 |
15 | private lateinit var binding: SecondFragmentBinding
16 |
17 | private val viewModel: SecondViewModel by viewModels()
18 |
19 | override fun onCreateView(
20 | inflater: LayoutInflater, container: ViewGroup?,
21 | savedInstanceState: Bundle?
22 | ): View {
23 | return SecondFragmentBinding.inflate(inflater).also {
24 | binding = it
25 | }.root
26 | }
27 |
28 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
29 | super.onViewCreated(view, savedInstanceState)
30 |
31 | binding.buttonSecond.setOnClickListener {
32 | findNavController().navigate(R.id.action_SecondFragment_to_FirstFragment)
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/thdev/app/ui/second/SecondViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app.ui.second
2 |
3 | import androidx.lifecycle.ViewModel
4 |
5 | class SecondViewModel : ViewModel() {
6 | // TODO: Implement the ViewModel
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/tech/thdev/app/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/java/tech/thdev/app/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.dynamicDarkColorScheme
8 | import androidx.compose.material3.dynamicLightColorScheme
9 | import androidx.compose.material3.lightColorScheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.platform.LocalContext
12 |
13 | private val DarkColorScheme = darkColorScheme(
14 | primary = Purple80,
15 | secondary = PurpleGrey80,
16 | tertiary = Pink80
17 | )
18 |
19 | private val LightColorScheme = lightColorScheme(
20 | primary = Purple40,
21 | secondary = PurpleGrey40,
22 | tertiary = Pink40
23 |
24 | /* Other default colors to override
25 | background = Color(0xFFFFFBFE),
26 | surface = Color(0xFFFFFBFE),
27 | onPrimary = Color.White,
28 | onSecondary = Color.White,
29 | onTertiary = Color.White,
30 | onBackground = Color(0xFF1C1B1F),
31 | onSurface = Color(0xFF1C1B1F),
32 | */
33 | )
34 |
35 | @Composable
36 | fun MyApplicationTheme(
37 | darkTheme: Boolean = isSystemInDarkTheme(),
38 | // Dynamic color is available on Android 12+
39 | dynamicColor: Boolean = true,
40 | content: @Composable () -> Unit
41 | ) {
42 | val colorScheme = when {
43 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
44 | val context = LocalContext.current
45 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
46 | }
47 |
48 | darkTheme -> DarkColorScheme
49 | else -> LightColorScheme
50 | }
51 |
52 | MaterialTheme(
53 | colorScheme = colorScheme,
54 | typography = Typography,
55 | content = content,
56 | )
57 | }
--------------------------------------------------------------------------------
/app/src/main/java/tech/thdev/app/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/content_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/first_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/main_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
20 |
21 |
22 |
23 |
24 |
25 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/second_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
17 |
18 |
23 |
24 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #BB86FC
4 | #6200EE
5 | #3700B3
6 | #03DAC6
7 | #018786
8 | #000000
9 | #FFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 16dp
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | My Application
3 | Settings
4 |
5 | First Fragment
6 | Second Fragment
7 | Next
8 | Previous
9 |
10 | Hello first fragment
11 | Hello second fragment. Arg: %1$s
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/test/java/tech/thdev/app/Counter.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app
2 |
3 | import org.junit.jupiter.api.Test
4 |
5 | class Counter {
6 | var count: Int = 0 //두 쓰레드에 의해 공유되는 변수
7 | val `object`: Any = Any()
8 |
9 | // @Synchronized
10 | /*synchronized*/ fun increment() {
11 | println("increment")
12 | synchronized(`object`) {
13 | println("before increment count $count")
14 | count++ //첫 번째 쓰레드에 의해 실행되는 문장
15 | println("after increment count $count")
16 | }
17 | }
18 |
19 | // @Synchronized
20 | /*synchronized*/ fun decrement() {
21 | println("decrement")
22 | // 동기화가 필요없는 다른 작업
23 | synchronized(`object`) {
24 | println("before decrement count $count")
25 | count--
26 | println("after decrement count $count")
27 | }
28 | }
29 |
30 | @Test
31 | @Throws(InterruptedException::class)
32 | fun testTwo() {
33 | val N = 30
34 |
35 | val sb = StringBuilder()
36 |
37 | for (i in 0..0) {
38 | val counter = Counter()
39 | val threadA = Thread(object : Runnable {
40 | override fun run() {
41 | run {
42 | var i = N
43 | while (i-- > 0) {
44 | counter.increment()
45 | }
46 | }
47 | }
48 | })
49 |
50 | val threadB = Thread {
51 | for (i in N downTo 1) {
52 | counter.decrement()
53 | }
54 | }
55 | threadA.start()
56 | threadB.start()
57 | threadA.join()
58 | threadB.join()
59 | println("count " + counter.count)
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/test/java/tech/thdev/app/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app
2 |
3 | import android.view.View
4 | import kotlinx.coroutines.async
5 | import kotlinx.coroutines.delay
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.collectLatest
8 | import kotlinx.coroutines.flow.flow
9 | import kotlinx.coroutines.launch
10 | import kotlinx.coroutines.test.runTest
11 | import org.junit.jupiter.api.Assertions
12 | import org.junit.jupiter.api.Test
13 | import tech.thdev.app.ui.JavaJava
14 | import tech.thdev.app.ui.SamplA
15 |
16 | /**
17 | * Example junit5 unit test, which will execute on the development machine (host).
18 | */
19 | class ExampleUnitTest {
20 |
21 | @Test
22 | fun addition_isCorrect() {
23 | Assertions.assertEquals(4, 2 + 2)
24 | // println("java ${JavaJava.getValue()}")
25 | println("java ${JavaJava.value}")
26 | println("new")
27 | val java = JavaJava.getInstance()
28 | println("java $java")
29 | println("SampleA A ${SamplA.A}")
30 | }
31 |
32 | @Test
33 | fun test() = runTest {
34 | val a = MutableStateFlow(0)
35 | launch {
36 | (0..9).forEach {
37 | a.value = it
38 | kotlinx.coroutines.delay(1)
39 | }
40 | }
41 |
42 | launch {
43 | a.collect {
44 | println("a -- $it")
45 | }
46 | }
47 |
48 | delay(1)
49 | launch {
50 | a.collect {
51 | println("b -- $it")
52 | }
53 | }
54 | delay(1)
55 | launch {
56 | a.collect {
57 | println("c -- $it")
58 | }
59 | }
60 | }
61 | }
62 |
63 |
--------------------------------------------------------------------------------
/app/src/test/java/tech/thdev/app/ui/first/FirstViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app.ui.first
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.ExperimentalCoroutinesApi
5 | import kotlinx.coroutines.test.UnconfinedTestDispatcher
6 | import kotlinx.coroutines.test.runTest
7 | import kotlinx.coroutines.test.setMain
8 | import org.junit.jupiter.api.Assertions
9 | import org.junit.jupiter.api.Test
10 | import org.mockito.kotlin.mock
11 | import org.mockito.kotlin.verify
12 | import org.mockito.kotlin.whenever
13 |
14 | class FirstViewModelTest {
15 |
16 | private val repository = mock()
17 |
18 | private val viewModel = FirstViewModel(repository = repository)
19 |
20 | @Test
21 | fun `test initData`() {
22 | Assertions.assertEquals(UiState(data = ""), viewModel._state.value)
23 | Assertions.assertEquals(UiState(data = ""), viewModel.state.value)
24 | }
25 |
26 | @OptIn(ExperimentalCoroutinesApi::class)
27 | @Test
28 | fun `test loadData`() = runTest {
29 | val testDispatcher = UnconfinedTestDispatcher(testScheduler)
30 | Dispatchers.setMain(testDispatcher)
31 |
32 | whenever(repository.getData()).thenReturn(10_000)
33 |
34 | viewModel.loadData()
35 |
36 | verify(repository).getData() // getData 잘 불러와졌는지
37 |
38 | Assertions.assertEquals("10100", viewModel.state.value.data)
39 | }
40 |
41 | @OptIn(ExperimentalCoroutinesApi::class)
42 | @Test
43 | fun `test loadData - fail case`() = runTest {
44 | val testDispatcher = UnconfinedTestDispatcher(testScheduler)
45 | Dispatchers.setMain(testDispatcher)
46 |
47 | whenever(repository.getData()).thenAnswer {
48 | throw Exception("error!!!!!!")
49 | }
50 |
51 | viewModel.loadData()
52 |
53 | verify(repository).getData() // getData 잘 불러와졌는지
54 |
55 | Assertions.assertEquals("Error", viewModel.state.value.data)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/test/java/tech/thdev/app/ui/first/MutableStateF.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app.ui.first
2 |
3 | import kotlinx.coroutines.flow.MutableStateFlow
4 | import kotlinx.coroutines.flow.StateFlow
5 | import kotlinx.coroutines.flow.asStateFlow
6 | import kotlinx.coroutines.flow.flow
7 | import kotlinx.coroutines.flow.map
8 | import kotlinx.coroutines.flow.onEach
9 | import kotlinx.coroutines.flow.stateIn
10 | import kotlinx.coroutines.test.runTest
11 | import org.junit.jupiter.api.Assertions
12 | import org.junit.jupiter.api.Test
13 |
14 | class MutableState : State {
15 |
16 | override var value: String = ""
17 | }
18 |
19 | interface State {
20 |
21 | val value: String
22 | }
23 |
24 | class ReadOnlyState(state: State) : State by state
25 |
26 | fun MutableState.asStateFlow(): State =
27 | ReadOnlyState(this)
28 |
29 | class MutableStateF {
30 |
31 | @Test
32 | fun test() = runTest {
33 | val state = MutableState().asStateFlow()
34 | // Assertions.assertTrue(state is MutableState)
35 |
36 | val runFlow = flow {
37 | emit("a")
38 | emit("b")
39 | emit("c")
40 | }
41 | val stateFlow = runFlow.stateIn(
42 | scope = this,
43 | started = kotlinx.coroutines.flow.SharingStarted.WhileSubscribed(),
44 | initialValue = ""
45 | )
46 | .collect {
47 | println(it)
48 | }
49 | // val stateFlow = MutableStateFlow("")
50 | // val newStateFlow = stateFlow.asStateFlow()
51 | // val new = stateFlow
52 | // .map {
53 | // "$it + "
54 | // }
55 | // .onEach {
56 | // println(it)
57 | // }
58 | //
59 | // Assertions.assertTrue(new is StateFlow)
60 | // val a = newStateFlow as StateFlow
61 |
62 | println(state.value)
63 | }
64 | }
--------------------------------------------------------------------------------
/architecture-one/README.md:
--------------------------------------------------------------------------------
1 | # Summary
2 | Android Architecture
3 |
4 | This code implements an architecture that mimics React's state management and reducers within an MVVM architecture. It utilizes uiTrigger for handling alerts and snackbars.
5 |
6 | Maybe MVI?
7 |
8 | ## Preview
9 |
10 | ### Default
11 |
12 | 
13 |
14 | ### ShowAlert
15 |
16 | 
17 |
18 | ### Show Snackbar
19 |
20 | 
21 |
22 | ## License
23 |
24 | ```
25 | Copyright 2025 Tae-hwan
26 |
27 | Licensed under the Apache License, Version 2.0 (the "License");
28 | you may not use this file except in compliance with the License.
29 | You may obtain a copy of the License at
30 |
31 | http://www.apache.org/licenses/LICENSE-2.0
32 |
33 | Unless required by applicable law or agreed to in writing, software
34 | distributed under the License is distributed on an "AS IS" BASIS,
35 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
36 | See the License for the specific language governing permissions and
37 | limitations under the License.
38 | ```
39 |
--------------------------------------------------------------------------------
/architecture-one/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/architecture-one/app/README.md:
--------------------------------------------------------------------------------
1 | # Summary
2 | Compose Navigation WebView Example
3 |
4 | ## Preview
5 |
6 | Navigation에서 WebView를 처음부터 재 생성하지 않고 사용한 결과 차이
7 |
8 | ## License
9 |
10 | ```
11 | Copyright 2024 Tae-hwan
12 |
13 | Licensed under the Apache License, Version 2.0 (the "License");
14 | you may not use this file except in compliance with the License.
15 | You may obtain a copy of the License at
16 |
17 | http://www.apache.org/licenses/LICENSE-2.0
18 |
19 | Unless required by applicable law or agreed to in writing, software
20 | distributed under the License is distributed on an "AS IS" BASIS,
21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 | See the License for the specific language governing permissions and
23 | limitations under the License.
24 | ```
25 |
--------------------------------------------------------------------------------
/architecture-one/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.application)
3 | alias(libs.plugins.tech.thdev.android.library.hilt)
4 | }
5 |
6 | setNamespace("architecture.app")
7 |
8 | android {
9 | val (majorVersion, minorVersion, patchVersion, code) = getVersionInfo()
10 |
11 | defaultConfig {
12 | applicationId = "tech.thdev.architecture.app"
13 | minSdk = libs.versions.minSdk.get().toInt()
14 | targetSdk = libs.versions.targetSdk.get().toInt()
15 | vectorDrawables.useSupportLibrary = true
16 | versionCode = code
17 | versionName = "$majorVersion.$minorVersion.$patchVersion"
18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
19 | multiDexEnabled = true
20 | }
21 | }
22 |
23 | ksp {
24 | arg("moduleName", project.name)
25 | arg("rootDir", rootDir.absolutePath)
26 | }
27 |
28 | dependencies {
29 | implementation(libs.compose.navigation)
30 | implementation(libs.google.material)
31 |
32 | implementation(projects.architectureOne.coreData.repository.alertRepository)
33 | implementation(projects.architectureOne.coreData.repository.alertRepositoryApi)
34 | implementation(projects.architectureOne.coreData.repository.snackbarRepository)
35 | implementation(projects.architectureOne.coreData.repository.snackbarRepositoryApi)
36 | }
37 |
--------------------------------------------------------------------------------
/architecture-one/app/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
22 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/java/tech/thdev/architecture/app/ArchitectureOneApplication.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class ArchitectureOneApplication : Application()
8 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/java/tech/thdev/architecture/app/common/alert/AlertDialogViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.common.alert
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import dagger.hilt.android.lifecycle.HiltViewModel
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.asStateFlow
8 | import kotlinx.coroutines.flow.launchIn
9 | import kotlinx.coroutines.flow.map
10 | import kotlinx.coroutines.flow.onEach
11 | import tech.thdev.architecture.app.alert.repository.api.InternalAlertRepository
12 | import tech.thdev.architecture.app.alert.repository.api.model.AlertEndEvent
13 | import tech.thdev.architecture.app.common.alert.model.AlertDialogUiState
14 | import javax.inject.Inject
15 |
16 | @HiltViewModel
17 | class AlertDialogViewModel @Inject constructor(
18 | private val internalAlertRepository: InternalAlertRepository,
19 | ) : ViewModel() {
20 |
21 | private val _alertDialogUiState = MutableStateFlow(null)
22 | val alertDialogUiState = _alertDialogUiState.asStateFlow()
23 |
24 | private val flowShow = internalAlertRepository.show()
25 | .map {
26 | AlertDialogUiState(
27 | title = it.title,
28 | message = it.message,
29 | confirmButtonText = it.confirmButtonText,
30 | dismissButtonText = it.dismissButtonText,
31 | show = true,
32 | )
33 | }
34 | .onEach {
35 | _alertDialogUiState.value = it
36 | }
37 |
38 | fun load() {
39 | flowShow
40 | .launchIn(viewModelScope)
41 | }
42 |
43 | fun actionDismiss() {
44 | _alertDialogUiState.value = null
45 | internalAlertRepository.endEvent(AlertEndEvent.Dismiss)
46 | }
47 |
48 | fun actionConfirm() {
49 | _alertDialogUiState.value = null
50 | internalAlertRepository.endEvent(AlertEndEvent.Confirm)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/java/tech/thdev/architecture/app/common/alert/model/AlertDialogUiState.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.common.alert.model
2 |
3 | import androidx.compose.runtime.Immutable
4 |
5 | @Immutable
6 | data class AlertDialogUiState(
7 | val show: Boolean,
8 | val title: String,
9 | val message: String,
10 | val confirmButtonText: String,
11 | val dismissButtonText: String,
12 | ) {
13 |
14 | companion object {
15 |
16 | val Default = AlertDialogUiState(
17 | show = false,
18 | title = "",
19 | message = "",
20 | confirmButtonText = "",
21 | dismissButtonText = "",
22 | )
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/java/tech/thdev/architecture/app/common/snackbar/SnackbarScreen.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.common.snackbar
2 |
3 | import androidx.compose.material3.SnackbarDuration
4 | import androidx.compose.material3.SnackbarHostState
5 | import androidx.compose.material3.SnackbarResult
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.LaunchedEffect
8 | import androidx.compose.runtime.getValue
9 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
10 | import androidx.lifecycle.viewmodel.compose.viewModel
11 |
12 | @Composable
13 | fun SnackbarScreen(
14 | snackbarHostState: SnackbarHostState,
15 | snackbarViewModel: SnackbarViewModel = viewModel(),
16 | ) {
17 | val snackbarUiState by snackbarViewModel.snackbarUiState.collectAsStateWithLifecycle()
18 |
19 | LaunchedEffect(snackbarUiState) {
20 | if (snackbarUiState != null) {
21 | val result = snackbarHostState
22 | .showSnackbar(
23 | message = snackbarUiState!!.message,
24 | actionLabel = snackbarUiState!!.actionLabel.takeIf { it.isNotEmpty() },
25 | duration = SnackbarDuration.Short,
26 | )
27 |
28 | when (result) {
29 | SnackbarResult.ActionPerformed -> snackbarViewModel.actionPerformed()
30 | SnackbarResult.Dismissed -> snackbarViewModel.actionDismiss()
31 | }
32 | }
33 | }
34 |
35 | LaunchedEffect(Unit) {
36 | snackbarViewModel.load()
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/java/tech/thdev/architecture/app/common/snackbar/SnackbarViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.common.snackbar
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import dagger.hilt.android.lifecycle.HiltViewModel
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.flow.MutableStateFlow
8 | import kotlinx.coroutines.flow.asStateFlow
9 | import kotlinx.coroutines.flow.flowOn
10 | import kotlinx.coroutines.flow.launchIn
11 | import kotlinx.coroutines.flow.map
12 | import kotlinx.coroutines.flow.onEach
13 | import tech.thdev.architecture.app.common.snackbar.model.SnackbarUiState
14 | import tech.thdev.architecture.app.snackbar.repository.api.InternalSnackbarRepository
15 | import tech.thdev.architecture.app.snackbar.repository.api.model.SnackbarEndEvent
16 | import javax.inject.Inject
17 |
18 | @HiltViewModel
19 | class SnackbarViewModel @Inject constructor(
20 | private val internalSnackbarRepository: InternalSnackbarRepository,
21 | ) : ViewModel() {
22 |
23 | private val _snackbarUiState = MutableStateFlow(null)
24 | val snackbarUiState = _snackbarUiState.asStateFlow()
25 |
26 | private val flowShow = internalSnackbarRepository.show()
27 | .map {
28 | SnackbarUiState(
29 | message = it.message,
30 | actionLabel = it.actionLabel,
31 | )
32 | }
33 | .onEach {
34 | _snackbarUiState.value = it
35 | }
36 | .flowOn(Dispatchers.Default)
37 |
38 | fun load() {
39 | flowShow
40 | .launchIn(viewModelScope)
41 | }
42 |
43 | fun actionDismiss() {
44 | _snackbarUiState.value = null
45 | internalSnackbarRepository.endEvent(SnackbarEndEvent.Dismiss)
46 | }
47 |
48 | fun actionPerformed() {
49 | _snackbarUiState.value = null
50 | internalSnackbarRepository.endEvent(SnackbarEndEvent.Performed)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/java/tech/thdev/architecture/app/common/snackbar/model/SnackbarUiState.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.common.snackbar.model
2 |
3 | import androidx.compose.runtime.Immutable
4 |
5 | @Immutable
6 | data class SnackbarUiState(
7 | val message: String,
8 | val actionLabel: String,
9 | )
10 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/java/tech/thdev/architecture/app/event/FlowComposeInteractionTrigger.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.event
2 |
3 | interface FlowComposeInteractionTrigger {
4 |
5 | fun action(event: MainEffect)
6 | }
7 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/java/tech/thdev/architecture/app/event/FlowInteraction.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.event
2 |
3 | import kotlinx.coroutines.channels.BufferOverflow
4 | import kotlinx.coroutines.flow.MutableSharedFlow
5 | import kotlinx.coroutines.flow.asSharedFlow
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 | @Singleton
10 | class FlowInteraction @Inject constructor() : FlowInteractionStream, FlowComposeInteractionTrigger {
11 |
12 | private val _event = MutableSharedFlow(
13 | extraBufferCapacity = 1,
14 | onBufferOverflow = BufferOverflow.DROP_OLDEST,
15 | )
16 |
17 | override val event = _event.asSharedFlow()
18 |
19 | override fun action(event: MainEffect) {
20 | _event.tryEmit(event)
21 | }
22 |
23 | override fun nextEvent(event: MainEffect) {
24 | action(event)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/java/tech/thdev/architecture/app/event/FlowInteractionStream.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.event
2 |
3 | import kotlinx.coroutines.flow.SharedFlow
4 |
5 | interface FlowInteractionStream {
6 |
7 | val event: SharedFlow
8 |
9 | fun nextEvent(event: MainEffect)
10 | }
11 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/java/tech/thdev/architecture/app/event/LocalComposeEventTriggerOwner.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.event
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ProvidedValue
5 | import androidx.compose.runtime.staticCompositionLocalOf
6 |
7 | object LocalComposeEventTriggerOwner {
8 |
9 | private val LocalComposition = staticCompositionLocalOf { noLocalProvidedFor("FlowComposeInteractionTrigger") }
10 |
11 | val current: FlowComposeInteractionTrigger
12 | @Composable
13 | get() = LocalComposition.current
14 |
15 | infix fun provides(registerOwner: FlowComposeInteractionTrigger): ProvidedValue =
16 | LocalComposition provides registerOwner
17 | }
18 |
19 | private fun noLocalProvidedFor(name: String): Nothing {
20 | error("CompositionLocal $name not present")
21 | }
22 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/java/tech/thdev/architecture/app/event/MainEffect.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.event
2 |
3 | sealed interface MainEffect {
4 |
5 | data object ShowAlert : MainEffect
6 |
7 | data object ShowAlertAndSnackbar : MainEffect
8 |
9 | data object None : MainEffect
10 | }
11 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/java/tech/thdev/architecture/app/event/di/EventModule.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.event.di
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import tech.thdev.architecture.app.event.FlowComposeInteractionTrigger
8 | import tech.thdev.architecture.app.event.FlowInteraction
9 | import tech.thdev.architecture.app.event.FlowInteractionStream
10 | import javax.inject.Singleton
11 |
12 | @Module
13 | @InstallIn(SingletonComponent::class)
14 | interface EventModule {
15 |
16 | @Binds
17 | @Singleton
18 | fun bindFlowInteractionStream(
19 | flowInteraction: FlowInteraction,
20 | ): FlowInteractionStream
21 |
22 | @Binds
23 | @Singleton
24 | fun bindFlowComposeInteractionTrigger(
25 | flowInteraction: FlowInteraction,
26 | ): FlowComposeInteractionTrigger
27 | }
28 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/java/tech/thdev/architecture/app/feature/model/MainUiState.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.feature.model
2 |
3 | data class MainUiState(
4 | val buttonEndEvent: String,
5 | ) {
6 |
7 | companion object {
8 |
9 | val Default = MainUiState(
10 | buttonEndEvent = "",
11 | )
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/java/tech/thdev/architecture/app/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/architecture-one/app/src/main/java/tech/thdev/architecture/app/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.dynamicDarkColorScheme
8 | import androidx.compose.material3.dynamicLightColorScheme
9 | import androidx.compose.material3.lightColorScheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.platform.LocalContext
12 |
13 | private val DarkColorScheme = darkColorScheme(
14 | primary = Purple80,
15 | secondary = PurpleGrey80,
16 | tertiary = Pink80
17 | )
18 |
19 | private val LightColorScheme = lightColorScheme(
20 | primary = Purple40,
21 | secondary = PurpleGrey40,
22 | tertiary = Pink40
23 |
24 | /* Other default colors to override
25 | background = Color(0xFFFFFBFE),
26 | surface = Color(0xFFFFFBFE),
27 | onPrimary = Color.White,
28 | onSecondary = Color.White,
29 | onTertiary = Color.White,
30 | onBackground = Color(0xFF1C1B1F),
31 | onSurface = Color(0xFF1C1B1F),
32 | */
33 | )
34 |
35 | @Composable
36 | fun MyApplicationTheme(
37 | darkTheme: Boolean = isSystemInDarkTheme(),
38 | // Dynamic color is available on Android 12+
39 | dynamicColor: Boolean = true,
40 | content: @Composable () -> Unit
41 | ) {
42 | val colorScheme = when {
43 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
44 | val context = LocalContext.current
45 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
46 | }
47 |
48 | darkTheme -> DarkColorScheme
49 | else -> LightColorScheme
50 | }
51 |
52 | MaterialTheme(
53 | colorScheme = colorScheme,
54 | typography = Typography,
55 | content = content
56 | )
57 | }
--------------------------------------------------------------------------------
/architecture-one/app/src/main/java/tech/thdev/architecture/app/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/drawable/baseline_close_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/drawable/baseline_home_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/drawable/baseline_web_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/architecture-one/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/architecture-one/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/architecture-one/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/architecture-one/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/architecture-one/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/architecture-one/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/architecture-one/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/architecture-one/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/architecture-one/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/architecture-one/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #BB86FC
4 | #6200EE
5 | #3700B3
6 | #03DAC6
7 | #018786
8 | #000000
9 | #FFFFFF
10 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 16dp
3 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | My Application
3 | Settings
4 |
5 | First Fragment
6 | Second Fragment
7 | Next
8 | Previous
9 |
10 | Hello first fragment
11 | Hello second fragment. Arg: %1$s
12 |
--------------------------------------------------------------------------------
/architecture-one/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/alert-repository-api/README.md:
--------------------------------------------------------------------------------
1 | # Summary
2 | Compose Navigation WebView Example
3 |
4 | ## Preview
5 |
6 | Navigation에서 WebView를 처음부터 재 생성하지 않고 사용한 결과 차이
7 |
8 | ## License
9 |
10 | ```
11 | Copyright 2024 Tae-hwan
12 |
13 | Licensed under the Apache License, Version 2.0 (the "License");
14 | you may not use this file except in compliance with the License.
15 | You may obtain a copy of the License at
16 |
17 | http://www.apache.org/licenses/LICENSE-2.0
18 |
19 | Unless required by applicable law or agreed to in writing, software
20 | distributed under the License is distributed on an "AS IS" BASIS,
21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 | See the License for the specific language governing permissions and
23 | limitations under the License.
24 | ```
25 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/alert-repository-api/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.library)
3 | }
4 |
5 | setNamespace("architecture.app.alert.repository.api")
6 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/alert-repository-api/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/alert-repository-api/src/main/java/tech/thdev/architecture/app/alert/repository/api/AlertRepository.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.alert.repository.api
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import tech.thdev.architecture.app.alert.repository.api.model.AlertEndEvent
5 | import tech.thdev.architecture.app.alert.repository.api.model.AlertItem
6 |
7 | interface AlertRepository {
8 |
9 | fun awaitShow(
10 | item: AlertItem,
11 | ): Flow
12 | }
13 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/alert-repository-api/src/main/java/tech/thdev/architecture/app/alert/repository/api/InternalAlertRepository.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.alert.repository.api
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import tech.thdev.architecture.app.alert.repository.api.model.AlertEndEvent
5 | import tech.thdev.architecture.app.alert.repository.api.model.AlertItem
6 |
7 | interface InternalAlertRepository {
8 |
9 | fun show(): Flow
10 |
11 | fun endEvent(alertEndEvent: AlertEndEvent)
12 | }
13 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/alert-repository-api/src/main/java/tech/thdev/architecture/app/alert/repository/api/model/AlertEndEvent.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.alert.repository.api.model
2 |
3 | enum class AlertEndEvent {
4 | Confirm,
5 | Dismiss,
6 | }
7 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/alert-repository-api/src/main/java/tech/thdev/architecture/app/alert/repository/api/model/AlertItem.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.alert.repository.api.model
2 |
3 | data class AlertItem(
4 | val title: String,
5 | val message: String,
6 | val confirmButtonText: String,
7 | val dismissButtonText: String,
8 | )
9 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/alert-repository/README.md:
--------------------------------------------------------------------------------
1 | # Summary
2 | Compose Navigation WebView Example
3 |
4 | ## Preview
5 |
6 | Navigation에서 WebView를 처음부터 재 생성하지 않고 사용한 결과 차이
7 |
8 | ## License
9 |
10 | ```
11 | Copyright 2024 Tae-hwan
12 |
13 | Licensed under the Apache License, Version 2.0 (the "License");
14 | you may not use this file except in compliance with the License.
15 | You may obtain a copy of the License at
16 |
17 | http://www.apache.org/licenses/LICENSE-2.0
18 |
19 | Unless required by applicable law or agreed to in writing, software
20 | distributed under the License is distributed on an "AS IS" BASIS,
21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 | See the License for the specific language governing permissions and
23 | limitations under the License.
24 | ```
25 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/alert-repository/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.library)
3 | alias(libs.plugins.tech.thdev.kotlin.library.hilt)
4 | }
5 |
6 | setNamespace("architecture.app.alert.repository")
7 |
8 | dependencies {
9 | implementation(projects.architectureOne.coreData.repository.alertRepositoryApi)
10 | }
11 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/alert-repository/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/alert-repository/src/main/java/tech/thdev/architecture/app/alert/repository/AlertRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.alert.repository
2 |
3 | import androidx.annotation.VisibleForTesting
4 | import dagger.Reusable
5 | import kotlinx.coroutines.channels.Channel
6 | import kotlinx.coroutines.flow.Flow
7 | import kotlinx.coroutines.flow.receiveAsFlow
8 | import tech.thdev.architecture.app.alert.repository.api.AlertRepository
9 | import tech.thdev.architecture.app.alert.repository.api.InternalAlertRepository
10 | import tech.thdev.architecture.app.alert.repository.api.model.AlertEndEvent
11 | import tech.thdev.architecture.app.alert.repository.api.model.AlertItem
12 | import javax.inject.Inject
13 |
14 | @Reusable
15 | class AlertRepositoryImpl @Inject constructor() : InternalAlertRepository, AlertRepository {
16 |
17 | @VisibleForTesting
18 | val channelShow = Channel(Channel.BUFFERED)
19 |
20 | @VisibleForTesting
21 | val channelEndEvent = Channel(Channel.BUFFERED)
22 |
23 | override fun show(): Flow =
24 | channelShow.receiveAsFlow()
25 |
26 | override fun awaitShow(item: AlertItem): Flow {
27 | channelShow.trySend(item)
28 | return channelEndEvent.receiveAsFlow()
29 | }
30 |
31 | override fun endEvent(alertEndEvent: AlertEndEvent) {
32 | channelEndEvent.trySend(alertEndEvent)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/alert-repository/src/main/java/tech/thdev/architecture/app/alert/repository/di/AlertRepositoryModel.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.alert.repository.di
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import tech.thdev.architecture.app.alert.repository.AlertRepositoryImpl
8 | import tech.thdev.architecture.app.alert.repository.api.AlertRepository
9 | import tech.thdev.architecture.app.alert.repository.api.InternalAlertRepository
10 | import javax.inject.Singleton
11 |
12 | @Module
13 | @InstallIn(SingletonComponent::class)
14 | abstract class AlertRepositoryModel {
15 |
16 | @Binds
17 | @Singleton
18 | abstract fun provideInternalAlertRepository(
19 | alertRepository: AlertRepositoryImpl,
20 | ): InternalAlertRepository
21 |
22 | @Binds
23 | @Singleton
24 | abstract fun provideAlertRepository(
25 | alertRepository: AlertRepositoryImpl,
26 | ): AlertRepository
27 | }
28 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/alert-repository/src/test/java/tech/thdev/app/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app
2 |
3 | import org.junit.jupiter.api.Assertions
4 | import org.junit.jupiter.api.Test
5 |
6 | /**
7 | * Example junit5 unit test, which will execute on the development machine (host).
8 | */
9 | class ExampleUnitTest {
10 |
11 | @Test
12 | fun addition_isCorrect() {
13 | Assertions.assertEquals(4, 2 + 2)
14 | }
15 | }
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/snackbar-repository-api/README.md:
--------------------------------------------------------------------------------
1 | # Summary
2 | Compose Navigation WebView Example
3 |
4 | ## Preview
5 |
6 | Navigation에서 WebView를 처음부터 재 생성하지 않고 사용한 결과 차이
7 |
8 | ## License
9 |
10 | ```
11 | Copyright 2024 Tae-hwan
12 |
13 | Licensed under the Apache License, Version 2.0 (the "License");
14 | you may not use this file except in compliance with the License.
15 | You may obtain a copy of the License at
16 |
17 | http://www.apache.org/licenses/LICENSE-2.0
18 |
19 | Unless required by applicable law or agreed to in writing, software
20 | distributed under the License is distributed on an "AS IS" BASIS,
21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 | See the License for the specific language governing permissions and
23 | limitations under the License.
24 | ```
25 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/snackbar-repository-api/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.library)
3 | }
4 |
5 | setNamespace("architecture.app.snackbar.repository.api")
6 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/snackbar-repository-api/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/snackbar-repository-api/src/main/java/tech/thdev/architecture/app/snackbar/repository/api/InternalSnackbarRepository.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.snackbar.repository.api
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import tech.thdev.architecture.app.snackbar.repository.api.model.SnackbarItem
5 | import tech.thdev.architecture.app.snackbar.repository.api.model.SnackbarEndEvent
6 |
7 | interface InternalSnackbarRepository {
8 |
9 | fun show(): Flow
10 |
11 | fun endEvent(endEvent: SnackbarEndEvent)
12 | }
13 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/snackbar-repository-api/src/main/java/tech/thdev/architecture/app/snackbar/repository/api/SnackbarRepository.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.snackbar.repository.api
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import tech.thdev.architecture.app.snackbar.repository.api.model.SnackbarItem
5 | import tech.thdev.architecture.app.snackbar.repository.api.model.SnackbarEndEvent
6 |
7 | interface SnackbarRepository {
8 |
9 | fun awaitShow(
10 | item: SnackbarItem,
11 | ): Flow
12 | }
13 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/snackbar-repository-api/src/main/java/tech/thdev/architecture/app/snackbar/repository/api/model/SnackbarEndEvent.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.snackbar.repository.api.model
2 |
3 | enum class SnackbarEndEvent {
4 | Performed,
5 | Dismiss,
6 | }
7 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/snackbar-repository-api/src/main/java/tech/thdev/architecture/app/snackbar/repository/api/model/SnackbarItem.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.snackbar.repository.api.model
2 |
3 | data class SnackbarItem(
4 | val message: String,
5 | val actionLabel: String,
6 | )
7 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/snackbar-repository/README.md:
--------------------------------------------------------------------------------
1 | # Summary
2 | Compose Navigation WebView Example
3 |
4 | ## Preview
5 |
6 | Navigation에서 WebView를 처음부터 재 생성하지 않고 사용한 결과 차이
7 |
8 | ## License
9 |
10 | ```
11 | Copyright 2024 Tae-hwan
12 |
13 | Licensed under the Apache License, Version 2.0 (the "License");
14 | you may not use this file except in compliance with the License.
15 | You may obtain a copy of the License at
16 |
17 | http://www.apache.org/licenses/LICENSE-2.0
18 |
19 | Unless required by applicable law or agreed to in writing, software
20 | distributed under the License is distributed on an "AS IS" BASIS,
21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 | See the License for the specific language governing permissions and
23 | limitations under the License.
24 | ```
25 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/snackbar-repository/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.library)
3 | alias(libs.plugins.tech.thdev.kotlin.library.hilt)
4 | }
5 |
6 | setNamespace("architecture.app.snackbar.repository")
7 |
8 | dependencies {
9 | implementation(projects.architectureOne.coreData.repository.snackbarRepositoryApi)
10 | }
11 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/snackbar-repository/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/snackbar-repository/src/main/java/tech/thdev/architecture/app/snackbar/repository/SnackbarRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.snackbar.repository
2 |
3 | import androidx.annotation.VisibleForTesting
4 | import kotlinx.coroutines.channels.Channel
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.receiveAsFlow
7 | import tech.thdev.architecture.app.snackbar.repository.api.InternalSnackbarRepository
8 | import tech.thdev.architecture.app.snackbar.repository.api.SnackbarRepository
9 | import tech.thdev.architecture.app.snackbar.repository.api.model.SnackbarItem
10 | import tech.thdev.architecture.app.snackbar.repository.api.model.SnackbarEndEvent
11 | import javax.inject.Inject
12 | import javax.inject.Singleton
13 |
14 | @Singleton
15 | class SnackbarRepositoryImpl @Inject constructor() : InternalSnackbarRepository, SnackbarRepository {
16 |
17 | @VisibleForTesting
18 | val channelShow = Channel(Channel.BUFFERED)
19 |
20 | @VisibleForTesting
21 | val channelEndEvent = Channel(Channel.BUFFERED)
22 |
23 | override fun show(): Flow =
24 | channelShow.receiveAsFlow()
25 |
26 | override fun awaitShow(item: SnackbarItem): Flow {
27 | channelShow.trySend(item)
28 | return channelEndEvent.receiveAsFlow()
29 | }
30 |
31 | override fun endEvent(endEvent: SnackbarEndEvent) {
32 | channelEndEvent.trySend(endEvent)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/snackbar-repository/src/main/java/tech/thdev/architecture/app/snackbar/repository/di/SnackbarRepositoryModel.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.architecture.app.snackbar.repository.di
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import tech.thdev.architecture.app.snackbar.repository.SnackbarRepositoryImpl
8 | import tech.thdev.architecture.app.snackbar.repository.api.InternalSnackbarRepository
9 | import tech.thdev.architecture.app.snackbar.repository.api.SnackbarRepository
10 | import javax.inject.Singleton
11 |
12 | @Module
13 | @InstallIn(SingletonComponent::class)
14 | abstract class SnackbarRepositoryModel {
15 |
16 | @Binds
17 | @Singleton
18 | abstract fun provideInternalSnackbarRepository(
19 | snackbarRepository: SnackbarRepositoryImpl,
20 | ): InternalSnackbarRepository
21 |
22 | @Binds
23 | @Singleton
24 | abstract fun provideSnackbarRepository(
25 | snackbarRepository: SnackbarRepositoryImpl,
26 | ): SnackbarRepository
27 | }
28 |
--------------------------------------------------------------------------------
/architecture-one/core-data/repository/snackbar-repository/src/test/java/tech/thdev/app/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app
2 |
3 | import org.junit.jupiter.api.Assertions
4 | import org.junit.jupiter.api.Test
5 |
6 | /**
7 | * Example junit5 unit test, which will execute on the development machine (host).
8 | */
9 | class ExampleUnitTest {
10 |
11 | @Test
12 | fun addition_isCorrect() {
13 | Assertions.assertEquals(4, 2 + 2)
14 | }
15 | }
--------------------------------------------------------------------------------
/architecture-one/images/sample_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/architecture-one/images/sample_01.png
--------------------------------------------------------------------------------
/architecture-one/images/sample_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/architecture-one/images/sample_02.png
--------------------------------------------------------------------------------
/architecture-one/images/sample_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/architecture-one/images/sample_03.png
--------------------------------------------------------------------------------
/build-logic/README.md:
--------------------------------------------------------------------------------
1 | # Convention Plugins
2 |
3 | The `build-logic` folder defines project-specific convention plugins, used to keep a single
4 | source of truth for common module configurations.
5 |
6 | This approach is heavily based on
7 | [https://developer.squareup.com/blog/herding-elephants/](https://developer.squareup.com/blog/herding-elephants/)
8 | and
9 | [https://github.com/jjohannes/idiomatic-gradle](https://github.com/jjohannes/idiomatic-gradle).
10 |
11 | By setting up convention plugins in `build-logic`, we can avoid duplicated build script setup,
12 | messy `subproject` configurations, without the pitfalls of the `buildSrc` directory.
13 |
14 | `build-logic` is an included build, as configured in the root
15 | [`settings.gradle.kts`](../settings.gradle.kts).
16 |
17 | Inside `build-logic` is a `convention` module, which defines a set of plugins that all normal
18 | modules can use to configure themselves.
19 |
20 | `build-logic` also includes a set of `Kotlin` files used to share logic between plugins themselves,
21 | which is most useful for configuring Android components (libraries vs applications) with shared
22 | code.
23 |
24 | These plugins are *additive* and *composable*, and try to only accomplish a single responsibility.
25 | Modules can then pick and choose the configurations they need.
26 | If there is one-off logic for a module without shared code, it's preferable to define that directly
27 | in the module's `build.gradle`, as opposed to creating a convention plugin with module-specific
28 | setup.
29 |
30 | Current list of convention plugins:
31 |
32 | - [`nowinandroid.android.application`](convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt),
33 | [`nowinandroid.android.library`](convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt),
34 | [`nowinandroid.android.test`](convention/src/main/kotlin/AndroidTestConventionPlugin.kt):
35 | Configures common Android and Kotlin options.
36 | - [`nowinandroid.android.application.compose`](convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt),
37 | [`nowinandroid.android.library.compose`](convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt):
38 | Configures Jetpack Compose options
39 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import tech.thdev.gradle.configureComposeAndroid
4 | import tech.thdev.gradle.configureComposeFeature
5 | import tech.thdev.gradle.configureKotlinAndroid
6 |
7 | class AndroidApplicationConventionPlugin : Plugin {
8 |
9 | override fun apply(target: Project) {
10 | with(target) {
11 | with(pluginManager) {
12 | apply("com.android.application")
13 | apply("org.jetbrains.kotlin.android")
14 | apply("com.google.devtools.ksp")
15 |
16 | apply("tech.thdev.android.library.unit.test")
17 |
18 | apply("org.jetbrains.kotlin.plugin.serialization")
19 |
20 | apply("tech.thdev.kotlin.library.verify.detekt")
21 | apply("tech.thdev.kotlin.library.verify.test")
22 | }
23 |
24 | configureKotlinAndroid()
25 | configureComposeFeature()
26 | configureComposeAndroid()
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import tech.thdev.gradle.configureComposeAndroid
4 | import tech.thdev.gradle.configureComposeFeature
5 |
6 | class AndroidLibraryComposeConventionPlugin : Plugin {
7 |
8 | override fun apply(target: Project) {
9 | with(target) {
10 | with(pluginManager) {
11 | apply("com.android.library")
12 | apply("org.jetbrains.kotlin.android")
13 | }
14 |
15 | configureComposeFeature()
16 | configureComposeAndroid()
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidLibraryComposeFeatureConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import org.gradle.kotlin.dsl.dependencies
4 | import tech.thdev.gradle.configureCoroutineAndroid
5 | import tech.thdev.gradle.extensions.findLibrary
6 |
7 | class AndroidLibraryComposeFeatureConventionPlugin : Plugin {
8 |
9 | override fun apply(target: Project) {
10 | with(target) {
11 | with(pluginManager) {
12 | apply("tech.thdev.android.library")
13 | apply("tech.thdev.android.library.compose")
14 | apply("tech.thdev.android.library.unit.test")
15 | }
16 |
17 | configureCoroutineAndroid()
18 |
19 | dependencies {
20 | implementation(findLibrary("androidx-activity"))
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import org.gradle.kotlin.dsl.dependencies
4 | import tech.thdev.gradle.configureKotlinAndroid
5 | import tech.thdev.gradle.extensions.findLibrary
6 |
7 | class AndroidLibraryConventionPlugin : Plugin {
8 |
9 | override fun apply(target: Project) {
10 | with(target) {
11 | with(pluginManager) {
12 | apply("com.android.library")
13 | apply("org.jetbrains.kotlin.android")
14 |
15 | apply("tech.thdev.android.library.unit.test")
16 | apply("tech.thdev.kotlin.library.ksp")
17 |
18 | apply("tech.thdev.kotlin.library.verify.detekt")
19 | apply("tech.thdev.kotlin.library.verify.test")
20 | }
21 |
22 | configureKotlinAndroid()
23 |
24 | dependencies {
25 | implementation(findLibrary("coroutines-core"))
26 | implementation(findLibrary("androidx-annotation"))
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidLibraryHiltConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import tech.thdev.gradle.configureDaggerHilt
4 |
5 | class AndroidLibraryHiltConventionPlugin : Plugin {
6 |
7 | override fun apply(target: Project) {
8 | with(target) {
9 | with(pluginManager) {
10 | apply("tech.thdev.kotlin.library.ksp")
11 | apply("com.google.dagger.hilt.android")
12 | }
13 |
14 | configureDaggerHilt()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidLibraryRobolectricConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import org.gradle.kotlin.dsl.dependencies
4 | import tech.thdev.gradle.extensions.androidExtension
5 | import tech.thdev.gradle.extensions.findLibrary
6 |
7 | /**
8 | * Robolectric 사용시 juni4의 @Test 어노테이션을 사용해야 정상 동작합니다.
9 | * 이외 임포트는 junit5껄 해도 동작하도록 호환 모듈을 포함합니다. junit5Vintage
10 | */
11 | class AndroidLibraryRobolectricConventionPlugin : Plugin {
12 |
13 | override fun apply(target: Project) {
14 | with(target) {
15 | with(pluginManager) {
16 | apply("com.android.library")
17 | apply("org.jetbrains.kotlin.android")
18 | }
19 |
20 | androidExtension.apply {
21 | @Suppress("UnstableApiUsage")
22 | testOptions {
23 | unitTests {
24 | // For Robolectric
25 | isIncludeAndroidResources = true
26 | }
27 | }
28 | }
29 |
30 | dependencies {
31 | testImplementation(findLibrary("test-androidx-core"))
32 | testImplementation(findLibrary("test-androidx-runner"))
33 | testImplementation(findLibrary("test-androidx-junit"))
34 | testImplementation(findLibrary("test-robolectric"))
35 | "testRuntimeOnly"(findLibrary("test-junit5-vintage"))
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidLibraryUnitTestConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import org.gradle.kotlin.dsl.dependencies
4 | import tech.thdev.gradle.configureUnitTest
5 | import tech.thdev.gradle.extensions.findLibrary
6 |
7 | class AndroidLibraryUnitTestConventionPlugin : Plugin {
8 |
9 | override fun apply(target: Project) {
10 | with(target) {
11 | configureUnitTest()
12 |
13 | dependencies {
14 | implementation(findLibrary("kotlin-stdlib"))
15 | implementation(findLibrary("coroutines-core"))
16 |
17 | testImplementation(findLibrary("test-mockito"))
18 | testImplementation(findLibrary("test-mockito-kotlin"))
19 | testImplementation(findLibrary("test-junit5"))
20 | "testRuntimeOnly"(findLibrary("test-junit5-engine"))
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/KotlinLibraryConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import tech.thdev.gradle.configureKotlinJvm
4 |
5 | class KotlinLibraryConventionPlugin : Plugin {
6 |
7 | override fun apply(target: Project) {
8 | with(target) {
9 | with(pluginManager) {
10 | apply("org.jetbrains.kotlin.jvm")
11 |
12 | apply("tech.thdev.android.library.unit.test")
13 |
14 | apply("tech.thdev.kotlin.library.verify.detekt")
15 | apply("tech.thdev.kotlin.library.verify.test")
16 | }
17 |
18 | configureKotlinJvm()
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/KotlinLibraryHiltConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import tech.thdev.gradle.configureDaggerKotlin
4 |
5 | class KotlinLibraryHiltConventionPlugin : Plugin {
6 |
7 | override fun apply(target: Project) {
8 | with(target) {
9 | with(pluginManager) {
10 | apply("tech.thdev.kotlin.library.ksp")
11 | }
12 |
13 | configureDaggerKotlin()
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/KotlinLibraryKspConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import com.google.devtools.ksp.gradle.KspExtension
2 | import org.gradle.api.Plugin
3 | import org.gradle.api.Project
4 | import org.gradle.kotlin.dsl.configure
5 | import tech.thdev.gradle.configureKspSourceSets
6 |
7 | class KotlinLibraryKspConventionPlugin : Plugin {
8 |
9 | override fun apply(target: Project) {
10 | with(target) {
11 | with(pluginManager) {
12 | apply("com.google.devtools.ksp")
13 | }
14 |
15 | configureKspSourceSets()
16 |
17 | extensions.configure {
18 | arg("moduleName", project.name)
19 | arg("rootDir", rootDir.absolutePath)
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/KotlinLibrarySerializationConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import org.gradle.kotlin.dsl.dependencies
4 | import tech.thdev.gradle.extensions.findLibrary
5 |
6 | class KotlinLibrarySerializationConventionPlugin : Plugin {
7 |
8 | override fun apply(target: Project) {
9 | with(target) {
10 | with(pluginManager) {
11 | apply("org.jetbrains.kotlin.plugin.serialization")
12 | }
13 |
14 | dependencies {
15 | implementation(findLibrary("kotlin-serializationJson"))
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/KotlinLibraryVerifyDetektConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import org.gradle.kotlin.dsl.dependencies
4 | import tech.thdev.gradle.configureVerifyDetekt
5 | import tech.thdev.gradle.extensions.findLibrary
6 |
7 | class KotlinLibraryVerifyDetektConventionPlugin : Plugin {
8 |
9 | override fun apply(target: Project) {
10 | with(target) {
11 | with(pluginManager) {
12 | apply("io.gitlab.arturbosch.detekt")
13 | }
14 |
15 | configureVerifyDetekt()
16 |
17 | dependencies {
18 | "detektPlugins"(findLibrary("verify-detektFormatting"))
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech.thdev.kotlin.library.verify.test.gradle.kts:
--------------------------------------------------------------------------------
1 | afterEvaluate {
2 | val tests = mutableListOf()
3 | val lints = mutableListOf()
4 | if (plugins.hasPlugin("java-library") || plugins.hasPlugin("java") || plugins.hasPlugin("kotlin")) {
5 | // test
6 | tests.add("test")
7 | } else {
8 | // test and lint
9 | if (project.name == "app") {
10 | // 앱 모듈만 flavor 옵션이 있어 devDebug 빌드타입으로 unit test 실행
11 | tests.add("testDevDebugUnitTest")
12 | } else {
13 | tests.add("testDebugUnitTest")
14 | }
15 | lints.add("lintDebug")
16 | }
17 |
18 | tasks.register("testAll") {
19 | dependsOn(*tests.toTypedArray())
20 | }
21 | tasks.register("lintAll") {
22 | if (lints.isNotEmpty()) {
23 | dependsOn(*lints.toTypedArray())
24 | } else {
25 | logger.log(LogLevel.DEBUG, "${project.name} has no lint")
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/ComposeAndroid.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.gradle
2 |
3 | import implementation
4 | import org.gradle.api.Project
5 | import org.gradle.kotlin.dsl.dependencies
6 | import tech.thdev.gradle.extensions.androidExtension
7 | import tech.thdev.gradle.extensions.findLibrary
8 |
9 | fun Project.configureComposeAndroid() {
10 | androidExtension.apply {
11 | dependencies {
12 | implementation(findLibrary("compose-keyboardState"))
13 |
14 | implementation(findLibrary("kotlin-collectionsImmutable"))
15 |
16 | implementation(findLibrary("compose-ui"))
17 | implementation(findLibrary("compose-foundation"))
18 | implementation(findLibrary("compose-material3"))
19 | implementation(findLibrary("compose-runtime"))
20 | implementation(findLibrary("compose-uiToolingPreview"))
21 | implementation(findLibrary("compose-constraintLayout"))
22 | implementation(findLibrary("compose-animation"))
23 |
24 | implementation(findLibrary("androidx-lifecycleRuntimeCompose"))
25 |
26 | implementation(findLibrary("image-coil"))
27 | implementation(findLibrary("image-coil-network"))
28 |
29 | "debugRuntimeOnly"(findLibrary("compose-uiTooling"))
30 | }
31 | }
32 | }
33 |
34 | /**
35 | * Compose Library
36 | */
37 | fun Project.configureComposeFeature() {
38 | androidExtension.apply {
39 | with(plugins) {
40 | apply("org.jetbrains.kotlin.plugin.compose")
41 | }
42 |
43 | buildFeatures {
44 | compose = true
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/CoroutineAndroid.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.gradle
2 |
3 | import implementation
4 | import org.gradle.api.Project
5 | import org.gradle.kotlin.dsl.dependencies
6 | import tech.thdev.gradle.extensions.findLibrary
7 |
8 | internal fun Project.configureCoroutineAndroid() {
9 | dependencies {
10 | implementation(findLibrary("coroutines-android"))
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/DaggerAndroid.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.gradle
2 |
3 | import implementation
4 | import org.gradle.api.Project
5 | import org.gradle.kotlin.dsl.dependencies
6 | import tech.thdev.gradle.extensions.findLibrary
7 |
8 | /**
9 | * Dagger Android 적용 시
10 | */
11 | internal fun Project.configureDaggerHilt() {
12 | configureDaggerKotlin()
13 |
14 | dependencies {
15 | implementation(findLibrary("dagger-hilt-android"))
16 | "ksp"(findLibrary("dagger-hilt-android-compiler"))
17 | }
18 | }
19 |
20 | internal fun Project.configureDaggerKotlin() {
21 | dependencies {
22 | implementation(findLibrary("dagger-hilt-core"))
23 | "ksp"(findLibrary("dagger-hilt-android-compiler"))
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/KotlinKsp.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.gradle
2 |
3 | import tech.thdev.gradle.extensions.androidExtension
4 | import kspSourceSet
5 | import org.gradle.api.Project
6 |
7 | /**
8 | * KSP Source sets
9 | * 모듈 용
10 | */
11 | internal fun Project.configureKspSourceSets() {
12 | androidExtension.apply {
13 | sourceSets.getByName("debug") {
14 | java.srcDir("debug".kspSourceSet)
15 | }
16 | sourceSets.getByName("release") {
17 | java.srcDir("release".kspSourceSet)
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/UnitTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.gradle
2 |
3 | import tech.thdev.gradle.extensions.findLibrary
4 | import implementation
5 | import org.gradle.api.Project
6 | import org.gradle.kotlin.dsl.dependencies
7 | import testImplementation
8 |
9 | /**
10 | * UnitTest dependencies
11 | */
12 | fun Project.configureUnitTest() {
13 | dependencies {
14 | implementation(findLibrary("kotlin.stdlib"))
15 | implementation(findLibrary("coroutines-core"))
16 |
17 | testImplementation(findLibrary("test-mockito"))
18 | testImplementation(findLibrary("test-mockito-kotlin"))
19 | testImplementation(findLibrary("test-junit5"))
20 | "testRuntimeOnly"(findLibrary("test-junit5-engine"))
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/VerifyDetekt.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.gradle
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.kotlin.dsl.withType
5 |
6 | internal fun Project.configureVerifyDetekt() {
7 | tasks.withType().configureEach {
8 | // Target version of the generated JVM bytecode. It is used for type resolution.
9 | jvmTarget = "17"
10 |
11 | buildUponDefaultConfig = true // preconfigure defaults
12 | allRules = false // activate all available (even unstable) rules.
13 | parallel = true
14 | config.setFrom(listOf(file("$rootDir/config/detekt/detekt.yml"))) // point to your custom config defining rules to run, overwriting default behavior
15 |
16 | reports {
17 | file("$rootDir/build/reports/test/${project.name}/").mkdirs()
18 | html.required.set(true) // observe findings in your browser with structure and code snippets
19 | html.outputLocation.set(file("$rootDir/build/reports/detekt/${project.name}.html"))
20 | xml.required.set(true) // checkstyle like format mainly for integrations like Jenkins
21 | xml.outputLocation.set(file("$rootDir/build/reports/detekt/${project.name}.xml"))
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/extensions/AppExtension.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("PackageDirectoryMismatch")
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.api.internal.catalog.DelegatingProjectDependency
5 | import tech.thdev.gradle.extensions.androidExtension
6 |
7 | fun Project.setNamespace(name: String) {
8 | androidExtension.apply {
9 | namespace = "tech.thdev.$name"
10 | }
11 | }
12 |
13 | val String.kspSourceSet: String
14 | get() = "build/generated/ksp/$this/kotlin"
15 |
16 | fun DelegatingProjectDependency.filterImplementation(
17 | body: (target: Project) -> Unit,
18 | ) {
19 | dependencyProject.allprojects.forEach {
20 | if (it.buildFile.isFile) {
21 | body(it)
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/extensions/ImportExtension.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("PackageDirectoryMismatch")
2 |
3 | import org.gradle.kotlin.dsl.DependencyHandlerScope
4 |
5 | internal fun DependencyHandlerScope.api(dependency: Any) {
6 | add("api", dependency)
7 | }
8 |
9 | internal fun DependencyHandlerScope.compileOnly(dependency: Any) {
10 | add("compileOnly", dependency)
11 | }
12 |
13 | internal fun DependencyHandlerScope.implementation(dependency: Any) {
14 | add("implementation", dependency)
15 | }
16 |
17 | internal fun DependencyHandlerScope.testImplementation(dependency: Any) {
18 | add("testImplementation", dependency)
19 | }
20 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/extensions/InternalProjectExtension.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.gradle.extensions
2 |
3 | import com.android.build.api.dsl.ApplicationExtension
4 | import com.android.build.api.dsl.CommonExtension
5 | import com.android.build.gradle.LibraryExtension
6 | import org.gradle.api.Project
7 | import org.gradle.api.artifacts.MinimalExternalModuleDependency
8 | import org.gradle.api.artifacts.VersionCatalog
9 | import org.gradle.api.artifacts.VersionCatalogsExtension
10 | import org.gradle.api.plugins.ExtensionContainer
11 | import org.gradle.api.provider.Provider
12 | import org.gradle.kotlin.dsl.getByType
13 |
14 | internal val Project.applicationExtension: CommonExtension<*, *, *, *, *, *>
15 | get() = extensions.getByType()
16 |
17 | internal val Project.libraryExtension: CommonExtension<*, *, *, *, *, *>
18 | get() = extensions.getByType()
19 |
20 | internal val Project.androidExtension: CommonExtension<*, *, *, *, *, *>
21 | get() = runCatching { libraryExtension }
22 | .recoverCatching { applicationExtension }
23 | .onFailure { println("Could not find Library or Application extension from this project") }
24 | .getOrThrow()
25 |
26 | internal val ExtensionContainer.libs: VersionCatalog
27 | get() = getByType().named("libs")
28 |
29 | internal fun Project.findLibrary(name: String): Provider =
30 | extensions.libs.findLibrary(name).get()
31 |
32 | internal fun Project.findVersion(name: String): String =
33 | extensions.libs.findVersion(name).get().requiredVersion
34 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/extensions/InternalVersionExtension.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("PackageDirectoryMismatch")
2 |
3 | import org.gradle.api.Project
4 |
5 | @Suppress("UnusedReceiverParameter")
6 | fun Project.getVersionInfo(): VersionInfo {
7 | return VersionInfo(
8 | majorVersion = "25",
9 | minorVersion = "1",
10 | patchVersion = "0",
11 | versionCode = 1,
12 | )
13 |
14 | // val versionPropsFile = file("${rootDir.absolutePath}/version.properties")
15 |
16 | // val majorVersion: String
17 | // val minorVersion: String
18 | // val patchVersion: String
19 | // val versionCode: Int
20 |
21 | // if (versionPropsFile.exists()) {
22 | // val versionProps = Properties()
23 | // versionProps.load(versionPropsFile.reader())
24 | //
25 | // versionCode = versionProps["versionCode"].toString().toInt()
26 | // majorVersion = versionProps["majorVersion"].toString()
27 | // minorVersion = versionProps["minorVersion"].toString()
28 | // patchVersion = versionProps["patchVersion"].toString()
29 | // return VersionInfo(
30 | // majorVersion = majorVersion,
31 | // minorVersion = minorVersion,
32 | // patchVersion = patchVersion,
33 | // versionCode = versionCode,
34 | // )
35 | // } else {
36 | // throw GradleException("version.properties 파일을 찾을 수 없습니다.")
37 | // }
38 | }
39 |
40 | data class VersionInfo(
41 | val majorVersion: String,
42 | val minorVersion: String,
43 | val patchVersion: String,
44 | val versionCode: Int,
45 | )
46 |
--------------------------------------------------------------------------------
/build-logic/gradle.properties:
--------------------------------------------------------------------------------
1 | # Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534
2 | org.gradle.parallel=true
3 | org.gradle.caching=true
4 | org.gradle.configureondemand=true
--------------------------------------------------------------------------------
/build-logic/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | dependencyResolutionManagement {
18 | repositories {
19 | google()
20 | mavenCentral()
21 | }
22 | versionCatalogs {
23 | create("libs") {
24 | from(files("../gradle/libs.versions.toml"))
25 | }
26 | }
27 | }
28 |
29 | rootProject.name = "build-logic"
30 | include(":convention")
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.android.library) apply false
5 | alias(libs.plugins.android.test) apply false
6 | alias(libs.plugins.compose.compiler) apply false
7 | alias(libs.plugins.kotlin.jvm) apply false
8 | alias(libs.plugins.kotlin.serialization) apply false
9 | alias(libs.plugins.ksp) apply false
10 | alias(libs.plugins.detekt) apply false
11 | alias(libs.plugins.android.dagger.hilt) apply false
12 | }
13 |
14 | task("clean", Delete::class) {
15 | delete(File(rootProject.projectDir, "build"))
16 | }
17 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
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 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Feb 11 13:14:25 KST 2024
2 | # https://gradle.org/releases/
3 | distributionBase=GRADLE_USER_HOME
4 | distributionPath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/README.md:
--------------------------------------------------------------------------------
1 | # Summary
2 | Compose Navigation WebView Example
3 |
4 | ## Preview
5 |
6 | Navigation에서 WebView를 처음부터 재 생성하지 않고 사용한 결과 차이
7 |
8 | ## License
9 |
10 | ```
11 | Copyright 2024 Tae-hwan
12 |
13 | Licensed under the Apache License, Version 2.0 (the "License");
14 | you may not use this file except in compliance with the License.
15 | You may obtain a copy of the License at
16 |
17 | http://www.apache.org/licenses/LICENSE-2.0
18 |
19 | Unless required by applicable law or agreed to in writing, software
20 | distributed under the License is distributed on an "AS IS" BASIS,
21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 | See the License for the specific language governing permissions and
23 | limitations under the License.
24 | ```
25 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.application)
3 | }
4 |
5 | setNamespace("compose.web.sample")
6 |
7 | android {
8 | val (majorVersion, minorVersion, patchVersion, code) = getVersionInfo()
9 |
10 | defaultConfig {
11 | applicationId = "tech.thdev.compose.web.sample"
12 | minSdk = libs.versions.minSdk.get().toInt()
13 | targetSdk = libs.versions.targetSdk.get().toInt()
14 | vectorDrawables.useSupportLibrary = true
15 | versionCode = code
16 | versionName = "$majorVersion.$minorVersion.$patchVersion"
17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
18 | multiDexEnabled = true
19 | }
20 | }
21 |
22 | dependencies {
23 | implementation(libs.google.material)
24 |
25 | implementation(libs.compose.navigation)
26 | }
27 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/image/main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/compose-example-01/image/main.png
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/image/web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/compose-example-01/image/web.png
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/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
22 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/java/tech/thdev/compose/web/sample/ui/design/system/button/ExampleButtonDefaults.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.compose.web.sample.ui.design.system.button
2 |
3 | import androidx.compose.foundation.layout.PaddingValues
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.Immutable
7 | import androidx.compose.runtime.State
8 | import androidx.compose.runtime.rememberUpdatedState
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.text.TextStyle
11 | import androidx.compose.ui.unit.dp
12 |
13 | object ExampleButtonDefaults {
14 |
15 | val MinHeight = 50.dp
16 |
17 | val ContentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 12.dp, bottom = 12.dp)
18 |
19 | val defaultTextStyle: TextStyle @Composable get() = MaterialTheme.typography.bodyMedium
20 |
21 | @Composable
22 | fun filledButtonColors(
23 | containerColor: Color = MaterialTheme.colorScheme.secondary,
24 | contentColor: Color = MaterialTheme.colorScheme.onSecondary,
25 | ): ExampleButtonColors = ExampleButtonColors(
26 | containerColor = containerColor,
27 | contentColor = contentColor,
28 | )
29 | }
30 |
31 | @Immutable
32 | data class ExampleButtonColors internal constructor(
33 | private val containerColor: Color,
34 | private val contentColor: Color,
35 | ) {
36 |
37 | @Composable
38 | internal fun containerColor(): State {
39 | return rememberUpdatedState(containerColor)
40 | }
41 |
42 | @Composable
43 | internal fun contentColor(): State {
44 | return rememberUpdatedState(contentColor)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/java/tech/thdev/compose/web/sample/ui/holder/home/component/TwoHomeItemEditComponent.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.compose.web.sample.ui.holder.home.component
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.TextField
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.runtime.setValue
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.tooling.preview.Preview
15 | import androidx.compose.ui.unit.dp
16 | import tech.thdev.compose.web.sample.ui.design.system.button.ExampleButton
17 | import tech.thdev.compose.web.sample.ui.model.ListItem
18 |
19 | @Composable
20 | internal fun TwoHomeItemEdit(
21 | item: ListItem.Item,
22 | onEditModeOff: (changeItem: ListItem.Item) -> Unit,
23 | onCancel: () -> Unit,
24 | ) {
25 | Column(
26 | modifier = Modifier
27 | .fillMaxWidth()
28 | .padding(16.dp)
29 | ) {
30 | var changeItem by remember(item) { mutableStateOf(item) }
31 | TextField(
32 | value = changeItem.text,
33 | onValueChange = { new ->
34 | changeItem = changeItem.copy(
35 | text = new,
36 | )
37 | },
38 | modifier = Modifier
39 | .fillMaxWidth()
40 | )
41 |
42 | Row {
43 | ExampleButton(
44 | text = "Save",
45 | onClick = { onEditModeOff(changeItem) },
46 | modifier = Modifier
47 | .weight(1f)
48 | )
49 |
50 | ExampleButton(
51 | text = "X",
52 | onClick = onCancel,
53 | modifier = Modifier
54 | .weight(1f)
55 | .padding(start = 10.dp)
56 | )
57 | }
58 | }
59 | }
60 |
61 | @Preview(showBackground = true)
62 | @Composable
63 | private fun PreviewTwoHomeItemEdit() {
64 | var item by remember {
65 | mutableStateOf(ListItem.Item.NEW)
66 | }
67 | TwoHomeItemEdit(
68 | item = item,
69 | onEditModeOff = { changeItem ->
70 | item = changeItem.copy(
71 | editMode = false,
72 | )
73 | },
74 | onCancel = {
75 | item = ListItem.Item.NEW
76 | },
77 | )
78 | }
79 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/java/tech/thdev/compose/web/sample/ui/holder/web/CustomWebChromeClient.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.compose.web.sample.ui.holder.web
2 |
3 | import android.os.Message
4 | import android.webkit.WebChromeClient
5 | import android.webkit.WebResourceRequest
6 | import android.webkit.WebView
7 | import android.webkit.WebView.WebViewTransport
8 | import android.webkit.WebViewClient
9 |
10 | class CustomWebChromeClient : WebChromeClient() {
11 |
12 | override fun onCreateWindow(view: WebView?, isDialog: Boolean, isUserGesture: Boolean, resultMsg: Message?): Boolean =
13 | if (view != null && resultMsg != null) {
14 | val newWebView = WebView(view.context).apply {
15 | webViewClient = object : WebViewClient() {
16 |
17 | override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean =
18 | request?.url?.let {
19 | if (request.isRedirect) {
20 | view?.loadUrl(it.toString())
21 | true
22 | } else {
23 | view?.loadUrl(it.toString())
24 | true
25 | }
26 | } ?: false
27 | }
28 | }
29 | (resultMsg.obj as WebViewTransport).apply {
30 | webView = newWebView
31 | }
32 | resultMsg.sendToTarget()
33 | true
34 | } else {
35 | super.onCreateWindow(view, isDialog, isUserGesture, resultMsg)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/java/tech/thdev/compose/web/sample/ui/holder/web/CustomWebViewClient.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.compose.web.sample.ui.holder.web
2 |
3 | import android.webkit.WebResourceRequest
4 | import android.webkit.WebView
5 | import android.webkit.WebViewClient
6 |
7 | class CustomWebViewClient : WebViewClient() {
8 |
9 | override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean =
10 | request?.url?.let {
11 | if (request.isRedirect) {
12 | view?.loadUrl(it.toString())
13 | true
14 | } else {
15 | view?.loadUrl(it.toString())
16 | true
17 | }
18 | } ?: false
19 | }
20 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/java/tech/thdev/compose/web/sample/ui/holder/web/LocalWebOwner.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.compose.web.sample.ui.holder.web
2 |
3 | import android.webkit.WebView
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.ProvidedValue
6 | import androidx.compose.runtime.compositionLocalOf
7 | import androidx.compose.runtime.staticCompositionLocalOf
8 |
9 | internal object LocalWebOwner {
10 |
11 | private val LocalComposition = compositionLocalOf { null }
12 |
13 | val current: WebView?
14 | @Composable
15 | get() = LocalComposition.current
16 |
17 | infix fun provides(registerOwner: WebView?): ProvidedValue =
18 | LocalComposition provides registerOwner
19 | }
20 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/java/tech/thdev/compose/web/sample/ui/model/ListItem.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.compose.web.sample.ui.model
2 |
3 | import androidx.compose.runtime.Immutable
4 |
5 | @Immutable
6 | data class ListItem(
7 | val items: List- ,
8 | ) {
9 |
10 | @Immutable
11 | data class Item(
12 | val index: Int,
13 | val text: String,
14 | val editMode: Boolean,
15 | ) {
16 |
17 | companion object {
18 |
19 | val NEW = Item(
20 | index = 0,
21 | text = "",
22 | editMode = true,
23 | )
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/java/tech/thdev/compose/web/sample/ui/model/NavigationSample.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.compose.web.sample.ui.model
2 |
3 | import androidx.compose.runtime.Immutable
4 |
5 | @Immutable
6 | data class NavigationSample(
7 | val title: String,
8 | val icon: Int,
9 | val trigger: Trigger
10 | ) {
11 |
12 | enum class Trigger {
13 | HOME,
14 | WEB,
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/java/tech/thdev/compose/web/sample/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.compose.web.sample.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/java/tech/thdev/compose/web/sample/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.compose.web.sample.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.dynamicDarkColorScheme
8 | import androidx.compose.material3.dynamicLightColorScheme
9 | import androidx.compose.material3.lightColorScheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.platform.LocalContext
12 |
13 | private val DarkColorScheme = darkColorScheme(
14 | primary = Purple80,
15 | secondary = PurpleGrey80,
16 | tertiary = Pink80
17 | )
18 |
19 | private val LightColorScheme = lightColorScheme(
20 | primary = Purple40,
21 | secondary = PurpleGrey40,
22 | tertiary = Pink40
23 |
24 | /* Other default colors to override
25 | background = Color(0xFFFFFBFE),
26 | surface = Color(0xFFFFFBFE),
27 | onPrimary = Color.White,
28 | onSecondary = Color.White,
29 | onTertiary = Color.White,
30 | onBackground = Color(0xFF1C1B1F),
31 | onSurface = Color(0xFF1C1B1F),
32 | */
33 | )
34 |
35 | @Composable
36 | fun MyApplicationTheme(
37 | darkTheme: Boolean = isSystemInDarkTheme(),
38 | // Dynamic color is available on Android 12+
39 | dynamicColor: Boolean = true,
40 | content: @Composable () -> Unit
41 | ) {
42 | val colorScheme = when {
43 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
44 | val context = LocalContext.current
45 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
46 | }
47 |
48 | darkTheme -> DarkColorScheme
49 | else -> LightColorScheme
50 | }
51 |
52 | MaterialTheme(
53 | colorScheme = colorScheme,
54 | typography = Typography,
55 | content = content
56 | )
57 | }
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/java/tech/thdev/compose/web/sample/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.compose.web.sample.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/drawable/baseline_close_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/drawable/baseline_home_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/drawable/baseline_web_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/compose-example-01/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/compose-example-01/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/compose-example-01/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/compose-example-01/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/compose-example-01/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/compose-example-01/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/compose-example-01/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/compose-example-01/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/compose-example-01/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/compose-example-01/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #BB86FC
4 | #6200EE
5 | #3700B3
6 | #03DAC6
7 | #018786
8 | #000000
9 | #FFFFFF
10 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 16dp
3 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | My Application
3 | Settings
4 |
5 | First Fragment
6 | Second Fragment
7 | Next
8 | Previous
9 |
10 | Hello first fragment
11 | Hello second fragment. Arg: %1$s
12 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/sample/compose/compose-example-01/src/test/java/tech/thdev/app/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app
2 |
3 | import org.junit.jupiter.api.Assertions
4 | import org.junit.jupiter.api.Test
5 |
6 | /**
7 | * Example junit5 unit test, which will execute on the development machine (host).
8 | */
9 | class ExampleUnitTest {
10 |
11 | @Test
12 | fun addition_isCorrect() {
13 | Assertions.assertEquals(4, 2 + 2)
14 | }
15 | }
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/README.md:
--------------------------------------------------------------------------------
1 | # Summary
2 | Compose Navigation WebView Example
3 |
4 | ## Preview
5 |
6 | Navigation에서 WebView를 처음부터 재 생성하지 않고 사용한 결과 차이
7 |
8 | - Before
9 | 
10 | - After
11 | 
12 |
13 | ## License
14 |
15 | ```
16 | Copyright 2024 Tae-hwan
17 |
18 | Licensed under the Apache License, Version 2.0 (the "License");
19 | you may not use this file except in compliance with the License.
20 | You may obtain a copy of the License at
21 |
22 | http://www.apache.org/licenses/LICENSE-2.0
23 |
24 | Unless required by applicable law or agreed to in writing, software
25 | distributed under the License is distributed on an "AS IS" BASIS,
26 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
27 | See the License for the specific language governing permissions and
28 | limitations under the License.
29 | ```
30 |
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.application)
3 | }
4 |
5 | setNamespace("compose.web.sample")
6 |
7 | android {
8 | val (majorVersion, minorVersion, patchVersion, code) = getVersionInfo()
9 |
10 | defaultConfig {
11 | applicationId = "tech.thdev.compose.web.sample"
12 | minSdk = libs.versions.minSdk.get().toInt()
13 | targetSdk = libs.versions.targetSdk.get().toInt()
14 | vectorDrawables.useSupportLibrary = true
15 | versionCode = code
16 | versionName = "$majorVersion.$minorVersion.$patchVersion"
17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
18 | multiDexEnabled = true
19 | }
20 | }
21 |
22 | dependencies {
23 | implementation(libs.google.material)
24 |
25 | implementation(libs.compose.navigation)
26 |
27 | implementation(libs.image.coil.compose)
28 | }
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/image/after.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/web-sample-01/image/after.png
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/image/before.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/web-sample-01/image/before.png
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/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
22 |
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/java/tech/thdev/compose/web/sample/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.compose.web.sample
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.enableEdgeToEdge
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.material3.Scaffold
11 | import androidx.compose.ui.Modifier
12 | import tech.thdev.compose.web.sample.ui.MainScreen
13 | import tech.thdev.compose.web.sample.ui.theme.MyApplicationTheme
14 |
15 | class MainActivity : ComponentActivity() {
16 | override fun onCreate(savedInstanceState: Bundle?) {
17 | super.onCreate(savedInstanceState)
18 | enableEdgeToEdge()
19 | setContent {
20 | MyApplicationTheme {
21 | Scaffold(
22 | modifier = Modifier
23 | .fillMaxSize()
24 | ) {
25 | Box(
26 | modifier = Modifier
27 | .padding(it)
28 | ) {
29 | MainScreen()
30 | }
31 | }
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/java/tech/thdev/compose/web/sample/ui/holder/other/OtherSampleScreen.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.compose.web.sample.ui.holder.other
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Alignment
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.tooling.preview.Preview
10 |
11 | @Composable
12 | internal fun OtherSampleScreen() {
13 | Box(
14 | contentAlignment = Alignment.Center,
15 | modifier = Modifier
16 | .fillMaxSize()
17 | ) {
18 | Text(
19 | text = "other screen",
20 | )
21 | }
22 | }
23 |
24 | @Preview(
25 | backgroundColor = 0xFFFFFF,
26 | )
27 | @Composable
28 | private fun PreviewOtherSampleScreen() {
29 | OtherSampleScreen()
30 | }
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/java/tech/thdev/compose/web/sample/ui/holder/web/LocalWebOwner.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.compose.web.sample.ui.holder.web
2 |
3 | import android.webkit.WebView
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.ProvidedValue
6 | import androidx.compose.runtime.staticCompositionLocalOf
7 |
8 | internal object LocalWebOwner {
9 |
10 | private val LocalComposition = staticCompositionLocalOf { null }
11 |
12 | val current: WebView?
13 | @Composable
14 | get() = LocalComposition.current
15 |
16 | infix fun provides(registerOwner: WebView?): ProvidedValue =
17 | LocalComposition provides registerOwner
18 | }
19 |
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/java/tech/thdev/compose/web/sample/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.compose.web.sample.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/java/tech/thdev/compose/web/sample/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.compose.web.sample.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.dynamicDarkColorScheme
8 | import androidx.compose.material3.dynamicLightColorScheme
9 | import androidx.compose.material3.lightColorScheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.platform.LocalContext
12 |
13 | private val DarkColorScheme = darkColorScheme(
14 | primary = Purple80,
15 | secondary = PurpleGrey80,
16 | tertiary = Pink80
17 | )
18 |
19 | private val LightColorScheme = lightColorScheme(
20 | primary = Purple40,
21 | secondary = PurpleGrey40,
22 | tertiary = Pink40
23 |
24 | /* Other default colors to override
25 | background = Color(0xFFFFFBFE),
26 | surface = Color(0xFFFFFBFE),
27 | onPrimary = Color.White,
28 | onSecondary = Color.White,
29 | onTertiary = Color.White,
30 | onBackground = Color(0xFF1C1B1F),
31 | onSurface = Color(0xFF1C1B1F),
32 | */
33 | )
34 |
35 | @Composable
36 | fun MyApplicationTheme(
37 | darkTheme: Boolean = isSystemInDarkTheme(),
38 | // Dynamic color is available on Android 12+
39 | dynamicColor: Boolean = true,
40 | content: @Composable () -> Unit
41 | ) {
42 | val colorScheme = when {
43 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
44 | val context = LocalContext.current
45 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
46 | }
47 |
48 | darkTheme -> DarkColorScheme
49 | else -> LightColorScheme
50 | }
51 |
52 | MaterialTheme(
53 | colorScheme = colorScheme,
54 | typography = Typography,
55 | content = content
56 | )
57 | }
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/java/tech/thdev/compose/web/sample/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.compose.web.sample.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/web-sample-01/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/web-sample-01/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/web-sample-01/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/web-sample-01/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/web-sample-01/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/web-sample-01/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/web-sample-01/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/web-sample-01/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/web-sample-01/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/compose/web-sample-01/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #BB86FC
4 | #6200EE
5 | #3700B3
6 | #03DAC6
7 | #018786
8 | #000000
9 | #FFFFFF
10 |
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 16dp
3 |
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | My Application
3 | Settings
4 |
5 | First Fragment
6 | Second Fragment
7 | Next
8 | Previous
9 |
10 | Hello first fragment
11 | Hello second fragment. Arg: %1$s
12 |
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/sample/compose/web-sample-01/src/test/java/tech/thdev/app/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app
2 |
3 | import org.junit.jupiter.api.Assertions
4 | import org.junit.jupiter.api.Test
5 |
6 | /**
7 | * Example junit5 unit test, which will execute on the development machine (host).
8 | */
9 | class ExampleUnitTest {
10 |
11 | @Test
12 | fun addition_isCorrect() {
13 | Assertions.assertEquals(4, 2 + 2)
14 | }
15 | }
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/README.md:
--------------------------------------------------------------------------------
1 | # Summary
2 | RecyclerView timer example
3 |
4 | ## Preview
5 |
6 | 2개의 RecyclerView를 활용하고 Coroutines을 활용하여 2개의 목록을 관리
7 |
8 | - A 리스트에는 10개의 아이템을 노출
9 | - B 리스트에는 A 목록에서 넘어온 데이터를 기반으로 노출하며 5초 후 A 리스트로 넘어가는 처리
10 |
11 | coroutines을 활용하여 각각의 아이템을 처리하는 샘플
12 |
13 | - Before
14 | 
15 | - After
16 | 
17 |
18 | ## License
19 |
20 | ```
21 | Copyright 2025 Tae-hwan
22 |
23 | Licensed under the Apache License, Version 2.0 (the "License");
24 | you may not use this file except in compliance with the License.
25 | You may obtain a copy of the License at
26 |
27 | http://www.apache.org/licenses/LICENSE-2.0
28 |
29 | Unless required by applicable law or agreed to in writing, software
30 | distributed under the License is distributed on an "AS IS" BASIS,
31 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
32 | See the License for the specific language governing permissions and
33 | limitations under the License.
34 | ```
35 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.application)
3 | }
4 |
5 | setNamespace("view.recycler.view.ex")
6 |
7 | android {
8 | val (majorVersion, minorVersion, patchVersion, code) = getVersionInfo()
9 |
10 | defaultConfig {
11 | applicationId = "tech.thdev.view.recycler.view.ex"
12 | minSdk = libs.versions.minSdk.get().toInt()
13 | targetSdk = libs.versions.targetSdk.get().toInt()
14 | vectorDrawables.useSupportLibrary = true
15 | versionCode = code
16 | versionName = "$majorVersion.$minorVersion.$patchVersion"
17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
18 | multiDexEnabled = true
19 | }
20 |
21 | buildFeatures {
22 | viewBinding = true
23 | }
24 | }
25 |
26 | dependencies {
27 | implementation(libs.androidx.appCompat)
28 | implementation(libs.androidx.activity)
29 | implementation(libs.androidx.recyclerView)
30 | implementation(libs.androidx.constraintLayout)
31 |
32 | implementation(libs.google.material)
33 |
34 | implementation(libs.coroutines.android)
35 | }
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/images/after.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/view/recycler-view-ex/images/after.png
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/images/before.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/view/recycler-view-ex/images/before.png
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/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
22 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/java/tech/thdev/view/recycler/view/ex/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.view.recycler.view.ex
2 |
3 | import android.os.Bundle
4 | import androidx.activity.viewModels
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.lifecycle.Lifecycle
7 | import androidx.lifecycle.lifecycleScope
8 | import androidx.lifecycle.repeatOnLifecycle
9 | import androidx.recyclerview.widget.LinearLayoutManager
10 | import kotlinx.coroutines.flow.collectLatest
11 | import kotlinx.coroutines.launch
12 | import tech.thdev.view.recycler.view.ex.adapter.MainAdapter
13 | import tech.thdev.view.recycler.view.ex.databinding.MainActivityBinding
14 |
15 | class MainActivity : AppCompatActivity() {
16 |
17 | private val viewModel: MainViewModel by viewModels()
18 |
19 | private val mainAdapter: MainAdapter by lazy {
20 | MainAdapter()
21 | }
22 |
23 | private val mainTrashAdapter: MainAdapter by lazy {
24 | MainAdapter()
25 | }
26 |
27 | private val binding: MainActivityBinding by lazy {
28 | MainActivityBinding.inflate(layoutInflater)
29 | }
30 |
31 | override fun onCreate(savedInstanceState: Bundle?) {
32 | super.onCreate(savedInstanceState)
33 | setContentView(binding.root)
34 | setSupportActionBar(binding.toolbar)
35 |
36 | binding.recyclerViewOne.run {
37 | adapter = mainAdapter
38 | layoutManager = LinearLayoutManager(this@MainActivity)
39 | }
40 | binding.recyclerViewTrash.run {
41 | adapter = mainTrashAdapter
42 | layoutManager = LinearLayoutManager(this@MainActivity)
43 | }
44 |
45 | mainAdapter.setOnClick {
46 | viewModel.actionClick(it)
47 | }
48 |
49 | mainTrashAdapter.setOnClick {
50 | viewModel.actionClickTwo(it)
51 | }
52 |
53 | lifecycleScope.launch {
54 | repeatOnLifecycle(Lifecycle.State.STARTED) {
55 | viewModel.listOne.collectLatest {
56 | mainAdapter.submitList(it)
57 | }
58 | }
59 | }
60 |
61 | lifecycleScope.launch {
62 | repeatOnLifecycle(Lifecycle.State.STARTED) {
63 | viewModel.trashList.collectLatest {
64 | mainTrashAdapter.submitList(it)
65 | }
66 | }
67 | }
68 |
69 | viewModel.loadData()
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/java/tech/thdev/view/recycler/view/ex/adapter/MainAdapter.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.view.recycler.view.ex.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.DiffUtil
6 | import androidx.recyclerview.widget.ListAdapter
7 | import androidx.recyclerview.widget.RecyclerView.ViewHolder
8 | import tech.thdev.view.recycler.view.ex.databinding.ItemViewBinding
9 | import tech.thdev.view.recycler.view.ex.databinding.TrashItemViewBinding
10 | import tech.thdev.view.recycler.view.ex.model.SampleItem
11 |
12 | class MainAdapter : ListAdapter(
13 | object : DiffUtil.ItemCallback() {
14 | override fun areItemsTheSame(oldItem: SampleItem, newItem: SampleItem): Boolean =
15 | oldItem.id == newItem.id
16 |
17 | override fun areContentsTheSame(oldItem: SampleItem, newItem: SampleItem): Boolean =
18 | oldItem == newItem
19 | }
20 | ) {
21 |
22 | private lateinit var onClick: (position: Int) -> Unit
23 |
24 | fun setOnClick(onClick: (position: Int) -> Unit) {
25 | this.onClick = onClick
26 | }
27 |
28 | override fun getItemViewType(position: Int): Int =
29 | if (getItem(position).trash) {
30 | TYPE_TRASH
31 | } else {
32 | TYPE_DEFAULT
33 | }
34 |
35 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
36 | when (viewType) {
37 | TYPE_TRASH -> MainTrashViewHolder(
38 | binding = TrashItemViewBinding.inflate(
39 | LayoutInflater.from(parent.context),
40 | parent,
41 | false
42 | ),
43 | onClick = onClick,
44 | )
45 |
46 | else -> MainViewHolder(
47 | binding = ItemViewBinding.inflate(
48 | LayoutInflater.from(parent.context),
49 | parent,
50 | false
51 | ),
52 | onClick = onClick,
53 | )
54 | }
55 |
56 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
57 | when (holder) {
58 | is MainViewHolder -> holder.bind(getItem(position))
59 | is MainTrashViewHolder -> holder.bind(getItem(position))
60 | }
61 | }
62 |
63 | companion object {
64 |
65 | const val TYPE_DEFAULT = 1_000
66 | const val TYPE_TRASH = 2_000
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/java/tech/thdev/view/recycler/view/ex/adapter/MainTrashViewHolder.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.view.recycler.view.ex.adapter
2 |
3 | import androidx.recyclerview.widget.RecyclerView.ViewHolder
4 | import tech.thdev.view.recycler.view.ex.databinding.TrashItemViewBinding
5 | import tech.thdev.view.recycler.view.ex.model.SampleItem
6 |
7 | class MainTrashViewHolder(
8 | private val binding: TrashItemViewBinding,
9 | private val onClick: (adapterIndex: Int) -> Unit,
10 | ) : ViewHolder(binding.root) {
11 |
12 | init {
13 | binding.root.setOnClickListener {
14 | onClick(absoluteAdapterPosition)
15 | }
16 | }
17 |
18 | fun bind(item: SampleItem) {
19 | binding.tvMessage.text = item.message
20 | binding.ivIcon.setImageResource(item.icon)
21 | binding.tvTimer.text = "Time out ${item.time}"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/java/tech/thdev/view/recycler/view/ex/adapter/MainViewHolder.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.view.recycler.view.ex.adapter
2 |
3 | import androidx.recyclerview.widget.RecyclerView.ViewHolder
4 | import tech.thdev.view.recycler.view.ex.databinding.ItemViewBinding
5 | import tech.thdev.view.recycler.view.ex.model.SampleItem
6 |
7 | class MainViewHolder(
8 | private val binding: ItemViewBinding,
9 | private val onClick: (adapterIndex: Int) -> Unit,
10 | ) : ViewHolder(binding.root) {
11 |
12 | init {
13 | binding.root.setOnClickListener {
14 | onClick(absoluteAdapterPosition)
15 | }
16 | }
17 |
18 | fun bind(item: SampleItem) {
19 | binding.tvMessage.text = item.message
20 | binding.ivIcon.setImageResource(item.icon)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/java/tech/thdev/view/recycler/view/ex/model/SampleItem.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.view.recycler.view.ex.model
2 |
3 | import androidx.annotation.DrawableRes
4 | import kotlinx.coroutines.Job
5 |
6 | data class SampleItem(
7 | val id: Int,
8 | val message: String,
9 | @DrawableRes val icon: Int,
10 | val time: Int = 10,
11 | val trash: Boolean = false,
12 | ) {
13 |
14 | var job: Job? = null
15 | }
16 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/drawable/baseline_perm_media_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/layout/item_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
22 |
23 |
36 |
37 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/layout/trash_item_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
21 |
22 |
34 |
35 |
50 |
51 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/view/recycler-view-ex/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/view/recycler-view-ex/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/view/recycler-view-ex/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/view/recycler-view-ex/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/view/recycler-view-ex/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/view/recycler-view-ex/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/view/recycler-view-ex/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/view/recycler-view-ex/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/view/recycler-view-ex/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/Android-BlogExample/bfec11a1a776623573771178c6af00ac5eb58630/sample/view/recycler-view-ex/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #BB86FC
4 | #6200EE
5 | #3700B3
6 | #03DAC6
7 | #018786
8 | #000000
9 | #FFFFFF
10 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 16dp
3 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | RecyclerEx
3 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/test/java/tech/thdev/app/Counter.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app
2 |
3 | import org.junit.jupiter.api.Test
4 |
5 | class Counter {
6 | var count: Int = 0 //두 쓰레드에 의해 공유되는 변수
7 | val `object`: Any = Any()
8 |
9 | // @Synchronized
10 | /*synchronized*/ fun increment() {
11 | println("increment")
12 | synchronized(`object`) {
13 | println("before increment count $count")
14 | count++ //첫 번째 쓰레드에 의해 실행되는 문장
15 | println("after increment count $count")
16 | }
17 | }
18 |
19 | // @Synchronized
20 | /*synchronized*/ fun decrement() {
21 | println("decrement")
22 | // 동기화가 필요없는 다른 작업
23 | synchronized(`object`) {
24 | println("before decrement count $count")
25 | count--
26 | println("after decrement count $count")
27 | }
28 | }
29 |
30 | @Test
31 | @Throws(InterruptedException::class)
32 | fun testTwo() {
33 | val N = 30
34 |
35 | val sb = StringBuilder()
36 |
37 | for (i in 0..0) {
38 | val counter = Counter()
39 | val threadA = Thread(object : Runnable {
40 | override fun run() {
41 | run {
42 | var i = N
43 | while (i-- > 0) {
44 | counter.increment()
45 | }
46 | }
47 | }
48 | })
49 |
50 | val threadB = Thread {
51 | for (i in N downTo 1) {
52 | counter.decrement()
53 | }
54 | }
55 | threadA.start()
56 | threadB.start()
57 | threadA.join()
58 | threadB.join()
59 | println("count " + counter.count)
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/test/java/tech/thdev/app/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app
2 |
3 | import android.view.View
4 | import kotlinx.coroutines.async
5 | import kotlinx.coroutines.delay
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.collectLatest
8 | import kotlinx.coroutines.flow.flow
9 | import kotlinx.coroutines.launch
10 | import kotlinx.coroutines.test.runTest
11 | import org.junit.jupiter.api.Assertions
12 | import org.junit.jupiter.api.Test
13 | import tech.thdev.app.ui.JavaJava
14 | import tech.thdev.app.ui.SamplA
15 |
16 | /**
17 | * Example junit5 unit test, which will execute on the development machine (host).
18 | */
19 | class ExampleUnitTest {
20 |
21 | @Test
22 | fun addition_isCorrect() {
23 | Assertions.assertEquals(4, 2 + 2)
24 | // println("java ${JavaJava.getValue()}")
25 | println("java ${JavaJava.value}")
26 | println("new")
27 | val java = JavaJava.getInstance()
28 | println("java $java")
29 | println("SampleA A ${SamplA.A}")
30 | }
31 |
32 | @Test
33 | fun test() = runTest {
34 | val a = MutableStateFlow(0)
35 | launch {
36 | (0..9).forEach {
37 | a.value = it
38 | kotlinx.coroutines.delay(1)
39 | }
40 | }
41 |
42 | launch {
43 | a.collect {
44 | println("a -- $it")
45 | }
46 | }
47 |
48 | delay(1)
49 | launch {
50 | a.collect {
51 | println("b -- $it")
52 | }
53 | }
54 | delay(1)
55 | launch {
56 | a.collect {
57 | println("c -- $it")
58 | }
59 | }
60 | }
61 | }
62 |
63 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/test/java/tech/thdev/app/ui/first/FirstViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app.ui.first
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.ExperimentalCoroutinesApi
5 | import kotlinx.coroutines.test.UnconfinedTestDispatcher
6 | import kotlinx.coroutines.test.runTest
7 | import kotlinx.coroutines.test.setMain
8 | import org.junit.jupiter.api.Assertions
9 | import org.junit.jupiter.api.Test
10 | import org.mockito.kotlin.mock
11 | import org.mockito.kotlin.verify
12 | import org.mockito.kotlin.whenever
13 |
14 | class FirstViewModelTest {
15 |
16 | private val repository = mock()
17 |
18 | private val viewModel = FirstViewModel(repository = repository)
19 |
20 | @Test
21 | fun `test initData`() {
22 | Assertions.assertEquals(UiState(data = ""), viewModel._state.value)
23 | Assertions.assertEquals(UiState(data = ""), viewModel.state.value)
24 | }
25 |
26 | @OptIn(ExperimentalCoroutinesApi::class)
27 | @Test
28 | fun `test loadData`() = runTest {
29 | val testDispatcher = UnconfinedTestDispatcher(testScheduler)
30 | Dispatchers.setMain(testDispatcher)
31 |
32 | whenever(repository.getData()).thenReturn(10_000)
33 |
34 | viewModel.loadData()
35 |
36 | verify(repository).getData() // getData 잘 불러와졌는지
37 |
38 | Assertions.assertEquals("10100", viewModel.state.value.data)
39 | }
40 |
41 | @OptIn(ExperimentalCoroutinesApi::class)
42 | @Test
43 | fun `test loadData - fail case`() = runTest {
44 | val testDispatcher = UnconfinedTestDispatcher(testScheduler)
45 | Dispatchers.setMain(testDispatcher)
46 |
47 | whenever(repository.getData()).thenAnswer {
48 | throw Exception("error!!!!!!")
49 | }
50 |
51 | viewModel.loadData()
52 |
53 | verify(repository).getData() // getData 잘 불러와졌는지
54 |
55 | Assertions.assertEquals("Error", viewModel.state.value.data)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/sample/view/recycler-view-ex/src/test/java/tech/thdev/app/ui/first/MutableStateF.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.app.ui.first
2 |
3 | import kotlinx.coroutines.flow.MutableStateFlow
4 | import kotlinx.coroutines.flow.StateFlow
5 | import kotlinx.coroutines.flow.asStateFlow
6 | import kotlinx.coroutines.flow.flow
7 | import kotlinx.coroutines.flow.map
8 | import kotlinx.coroutines.flow.onEach
9 | import kotlinx.coroutines.flow.stateIn
10 | import kotlinx.coroutines.test.runTest
11 | import org.junit.jupiter.api.Assertions
12 | import org.junit.jupiter.api.Test
13 |
14 | class MutableState : State {
15 |
16 | override var value: String = ""
17 | }
18 |
19 | interface State {
20 |
21 | val value: String
22 | }
23 |
24 | class ReadOnlyState(state: State) : State by state
25 |
26 | fun MutableState.asStateFlow(): State =
27 | ReadOnlyState(this)
28 |
29 | class MutableStateF {
30 |
31 | @Test
32 | fun test() = runTest {
33 | val state = MutableState().asStateFlow()
34 | // Assertions.assertTrue(state is MutableState)
35 |
36 | val runFlow = flow {
37 | emit("a")
38 | emit("b")
39 | emit("c")
40 | }
41 | val stateFlow = runFlow.stateIn(
42 | scope = this,
43 | started = kotlinx.coroutines.flow.SharingStarted.WhileSubscribed(),
44 | initialValue = ""
45 | )
46 | .collect {
47 | println(it)
48 | }
49 | // val stateFlow = MutableStateFlow("")
50 | // val newStateFlow = stateFlow.asStateFlow()
51 | // val new = stateFlow
52 | // .map {
53 | // "$it + "
54 | // }
55 | // .onEach {
56 | // println(it)
57 | // }
58 | //
59 | // Assertions.assertTrue(new is StateFlow)
60 | // val a = newStateFlow as StateFlow
61 |
62 | println(state.value)
63 | }
64 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | pluginManagement {
4 | includeBuild("build-logic")
5 | repositories {
6 | google {
7 | content {
8 | includeGroupByRegex("com\\.android.*")
9 | includeGroupByRegex("com\\.google.*")
10 | includeGroupByRegex("androidx.*")
11 | }
12 | }
13 | mavenCentral()
14 | gradlePluginPortal()
15 | }
16 | }
17 |
18 | dependencyResolutionManagement {
19 | repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
20 |
21 | repositories {
22 | google()
23 | mavenCentral()
24 | }
25 | }
26 |
27 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
28 |
29 | rootProject.name = "thdev-example"
30 |
31 | include(":app")
32 |
33 | include(
34 | ":architecture-one:app",
35 | ":architecture-one:core-data:repository:alert-repository",
36 | ":architecture-one:core-data:repository:alert-repository-api",
37 | ":architecture-one:core-data:repository:snackbar-repository",
38 | ":architecture-one:core-data:repository:snackbar-repository-api",
39 | )
40 |
41 | include(
42 | ":sample:compose:web-sample-01",
43 | ":sample:compose:compose-example-01",
44 | ":sample:view:recycler-view-ex",
45 | )
46 |
--------------------------------------------------------------------------------