├── .circleci
└── config.yml
├── .editorconfig
├── .github
├── dependabot.yml
└── workflows
│ ├── apply-ktlint.yml
│ └── generate-dependency-graph.yml
├── .gitignore
├── .idea
├── .gitignore
├── compiler.xml
├── gradle.xml
├── jarRepositories.xml
├── saveactions_settings.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── jp
│ │ └── dosukoi
│ │ └── githubclientforjetpackcompose
│ │ ├── App.kt
│ │ ├── AppModule.kt
│ │ └── RepositoryModule.kt
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ └── ic_launcher_background.xml
│ ├── layout
│ └── activity_main.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-mdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── values-night
│ └── themes.xml
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
├── build.gradle.kts
├── buildSrc
├── build.gradle.kts
└── src
│ └── main
│ └── kotlin
│ ├── Common.kt
│ └── ProjectDependencyGraphTask.kt
├── data
├── api
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── jp
│ │ └── dosukoi
│ │ └── data
│ │ └── api
│ │ └── common
│ │ ├── AccessTokenProvider.kt
│ │ ├── IApiType.kt
│ │ └── IAuthApiType.kt
├── repository
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── jp
│ │ └── dosukoi
│ │ └── data
│ │ └── repository
│ │ ├── auth
│ │ └── AuthRepositoryImpl.kt
│ │ ├── common
│ │ ├── ApiTransaction.kt
│ │ ├── AppDatabase.kt
│ │ └── LiveDataExt.kt
│ │ ├── myPage
│ │ ├── ReposRepositoryImpl.kt
│ │ └── UserRepositoryImpl.kt
│ │ └── search
│ │ └── SearchRepositoryImpl.kt
└── usecase
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src
│ ├── androidTest
│ └── java
│ │ └── jp
│ │ └── dosukoi
│ │ └── data
│ │ └── usecase
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ └── AndroidManifest.xml
│ └── test
│ └── java
│ └── jp
│ └── dosukoi
│ └── data
│ └── usecase
│ └── ExampleUnitTest.kt
├── domain
├── entity
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── jp
│ │ └── dosukoi
│ │ └── githubclient
│ │ └── domain
│ │ └── entity
│ │ ├── auth
│ │ ├── Auth.kt
│ │ ├── AuthDao.kt
│ │ └── UnAuthorizeException.kt
│ │ ├── myPage
│ │ ├── Repository.kt
│ │ ├── User.kt
│ │ └── UserStatus.kt
│ │ └── search
│ │ └── Search.kt
├── repository
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── jp
│ │ └── dosukoi
│ │ └── githubclient
│ │ └── domain
│ │ └── repository
│ │ ├── auth
│ │ └── AuthRepository.kt
│ │ ├── myPage
│ │ ├── ReposRepository.kt
│ │ └── UserRepository.kt
│ │ └── search
│ │ └── SearchRepository.kt
└── usecase
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── jp
│ └── dosukoi
│ └── githubclient
│ └── domain
│ └── usecase
│ ├── auth
│ └── GetAccessTokenUseCase.kt
│ ├── myPage
│ ├── GetRepositoriesUseCase.kt
│ └── GetUserStatusUseCase.kt
│ └── search
│ └── GetSearchDataUseCase.kt
├── gradle.properties
├── gradle
├── dependency-graph
│ ├── project.dot
│ └── project.dot.png
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
├── testing
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── jp
│ │ └── dosukoi
│ │ └── testing
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── jp
│ │ └── dosukoi
│ │ └── testing
│ │ └── common
│ │ ├── Assertion.kt
│ │ ├── MainCoroutineRule.kt
│ │ └── TestLiveData.kt
│ └── test
│ └── java
│ └── jp
│ └── dosukoi
│ └── testing
│ └── ExampleUnitTest.kt
└── ui
├── view
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── jp
│ │ └── dosukoi
│ │ └── ui
│ │ └── view
│ │ ├── common
│ │ ├── ActivityNavigation.kt
│ │ ├── AppBar.kt
│ │ ├── AppColors.kt
│ │ ├── CompositionLocalProvider.kt
│ │ ├── LazyListStateEx.kt
│ │ ├── LoadingAndErrorScreen.kt
│ │ └── ViewModelFactory.kt
│ │ ├── myPage
│ │ ├── MyPageComponent.kt
│ │ ├── MyPageScreen.kt
│ │ └── UnAuthenticatedUserComponent.kt
│ │ ├── search
│ │ ├── SearchComponent.kt
│ │ └── SearchPageScreen.kt
│ │ ├── top
│ │ ├── MainActivity.kt
│ │ └── TopScreen.kt
│ │ └── widget
│ │ └── GlanceAppWidgetSample.kt
│ └── res
│ ├── drawable-anydpi
│ ├── ic_arrow_back.xml
│ ├── ic_my_page.xml
│ └── ic_search.xml
│ ├── drawable-hdpi
│ ├── ic_arrow_back.png
│ ├── ic_my_page.png
│ └── ic_search.png
│ ├── drawable-mdpi
│ ├── ic_arrow_back.png
│ ├── ic_my_page.png
│ └── ic_search.png
│ ├── drawable-xhdpi
│ ├── ic_arrow_back.png
│ ├── ic_my_page.png
│ └── ic_search.png
│ ├── drawable-xxhdpi
│ ├── ic_arrow_back.png
│ ├── ic_my_page.png
│ └── ic_search.png
│ ├── layout
│ └── glance_initial_layout.xml
│ ├── raw
│ ├── error_animation.json
│ └── loading_animation.json
│ ├── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
│ ├── xml-v31
│ └── glance_app_widget_sample_meta_data.xml
│ └── xml
│ └── glance_app_widget_sample_meta_data.xml
└── viewModel
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
├── main
├── AndroidManifest.xml
└── java
│ └── jp
│ └── dosukoi
│ └── ui
│ └── viewmodel
│ ├── common
│ ├── LoadState.kt
│ ├── NoCacheMutableLiveData.kt
│ └── WebViewExt.kt
│ ├── myPage
│ ├── MyPageUiState.kt
│ └── MyPageViewModel.kt
│ └── search
│ ├── SearchUiState.kt
│ └── SearchViewModel.kt
└── test
└── java
└── jp
└── dosukoi
└── ui
└── viewmodel
├── myPage
└── MyPageViewModelUnitTest.kt
└── search
└── SearchViewModelUnitTest.kt
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | orbs:
4 | android: circleci/android@1.0.3
5 |
6 | jobs:
7 | build_and_test:
8 | executor:
9 | name: android/android-machine
10 | steps:
11 | - checkout
12 | - run:
13 | name: run build
14 | command: ./gradlew compileDebugSources
15 | - run:
16 | name: run test
17 | command: ./gradlew test
18 |
19 | workflows:
20 | build_and_test:
21 | jobs:
22 | - build_and_test
23 |
24 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | charset = utf-8
3 | indent_style = space
4 | indent_size = 4
5 | end_of_line = lf
6 | insert_final_newline = true
7 |
8 | [*.json]
9 | indent_style = space
10 | indent_size = 2
11 |
12 | [{*.yml, *.yaml}]
13 | indent_style = space
14 | indent_size = 2
15 |
16 | [{*.kt, *.kts}]
17 | max_line_length = off
18 | disabled_rules = import-ordering, no-wildcard-imports, parameter-list-wrapping, indent
19 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: gradle
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 |
--------------------------------------------------------------------------------
/.github/workflows/apply-ktlint.yml:
--------------------------------------------------------------------------------
1 | name: ApplyKtlint
2 |
3 | on:
4 | pull_request
5 |
6 | jobs:
7 | applyKtlint:
8 | runs-on: ubuntu-latest
9 | if: ${{ !github.event.pull_request.draft }}
10 |
11 | steps:
12 | - uses: actions/checkout@v2
13 | with:
14 | ref: ${{ github.event.pull_request.head.ref }}
15 | - name: set up JDK 1.8
16 | uses: actions/setup-java@v2
17 | with:
18 | java-version: 11
19 | distribution: 'adopt'
20 | - name: Ktlint Format
21 | run: ./gradlew ktlintFormat
22 | - name: Commit formatted
23 | run: |
24 | git config --local user.email "dosukoroid@gmail.com"
25 | git config --local user.name "Naoki-Hidaka"
26 | git commit -am "Apply ktlint Format" && git push origin HEAD
27 | exit 0
28 |
--------------------------------------------------------------------------------
/.github/workflows/generate-dependency-graph.yml:
--------------------------------------------------------------------------------
1 | name: generate dependency graph
2 |
3 | on:
4 | pull_request
5 |
6 | jobs:
7 | generate-dependency-graph:
8 | name: Generate Dependency Graph
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v2
13 | with:
14 | ref: ${{ github.event.pull_request.head.ref }}
15 |
16 | - name: Setup Graphviz
17 | uses: ts-graphviz/setup-graphviz@v1
18 |
19 | - name: Generate Dependency Graph
20 | run: ./gradlew projectDependencyGraph
21 |
22 | - name: Commmit
23 | run: |
24 | git config --local user.email "dosukoroid@gmail.com"
25 | git config --local user.name "Naoki-Hidaka"
26 | git commit -am "Update dependency graph" && git push origin HEAD
27 | exit 0
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | /.idea/misc.xml
11 | .DS_Store
12 | /build
13 | /captures
14 | .externalNativeBuild
15 | .cxx
16 | local.properties
17 | */build
18 | */*/build
19 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
32 |
33 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/.idea/saveactions_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | kotlin("android")
4 | kotlin("kapt")
5 | id("dagger.hilt.android.plugin")
6 | id("androidx.navigation.safeargs.kotlin")
7 | id("kotlin-parcelize")
8 | }
9 |
10 | android {
11 | compileSdk = 31
12 | buildToolsVersion = "30.0.3"
13 |
14 | defaultConfig {
15 | applicationId = "jp.dosukoi.githubclientforjetpackcompose"
16 | minSdk = 26
17 | targetSdk = 31
18 | versionCode = 1
19 | versionName = "1.0.0"
20 |
21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
22 | }
23 |
24 | buildTypes {
25 | getByName("release") {
26 | isMinifyEnabled = false
27 | proguardFiles(
28 | getDefaultProguardFile("proguard-android-optimize.txt"),
29 | "proguard-rules.pro"
30 | )
31 | }
32 | }
33 |
34 | compileOptions {
35 | sourceCompatibility = JavaVersion.VERSION_1_8
36 | targetCompatibility = JavaVersion.VERSION_1_8
37 | }
38 | kotlinOptions {
39 | jvmTarget = "1.8"
40 | }
41 |
42 | buildFeatures {
43 | compose = true
44 | dataBinding = true
45 | }
46 | composeOptions {
47 | kotlinCompilerExtensionVersion = "1.0.0"
48 | }
49 |
50 | packagingOptions {
51 | exclude("META-INF/AL2.0")
52 | exclude("META-INF/LGPL2.1")
53 | exclude("**/attach_hotspot_windows.dll")
54 | exclude("META-INF/licenses/**")
55 | }
56 | }
57 |
58 | dependencies {
59 |
60 | implementation(project(":ui:view"))
61 | implementation(project(":ui:viewModel"))
62 | implementation(project(":data:api"))
63 | implementation(project(":data:repository"))
64 | implementation(project(":domain:usecase"))
65 | implementation(project(":domain:entity"))
66 | implementation(project(":domain:repository"))
67 |
68 |
69 | implementation("androidx.core:core-ktx:1.7.0")
70 | implementation("androidx.appcompat:appcompat:1.4.1")
71 | implementation("com.google.android.material:material:1.5.0")
72 | implementation("androidx.constraintlayout:constraintlayout:2.1.3")
73 |
74 | implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
75 | // Compose
76 | val composeVersion = "1.1.1"
77 | implementation("androidx.compose.ui:ui:$composeVersion")
78 | implementation("androidx.compose.runtime:runtime:$composeVersion")
79 | implementation("androidx.compose.runtime:runtime-livedata:$composeVersion")
80 | implementation("androidx.compose.material:material:$composeVersion")
81 | implementation("androidx.compose.foundation:foundation:$composeVersion")
82 | implementation("androidx.compose.compiler:compiler:$composeVersion")
83 | implementation("androidx.compose.animation:animation:$composeVersion")
84 | implementation("androidx.compose.foundation:foundation-layout:$composeVersion")
85 | implementation("androidx.compose.ui:ui-tooling:$composeVersion")
86 | implementation("androidx.compose.ui:ui-util:$composeVersion")
87 | implementation("com.google.accompanist:accompanist-pager:0.23.1")
88 |
89 | // Glance
90 | val glanceVersion = "1.0.0-SNAPSHOT"
91 | implementation("androidx.glance:glance:$glanceVersion")
92 | implementation("androidx.glance:glance-appwidget:$glanceVersion")
93 |
94 | // Hilt
95 | val hiltVersion = "2.41"
96 | val hiltJetpackVersion = "1.0.0"
97 | implementation("com.google.dagger:hilt-android:$hiltVersion")
98 |
99 | kapt("com.google.dagger:hilt-android-compiler:$hiltVersion")
100 | kapt("androidx.hilt:hilt-compiler:$hiltJetpackVersion")
101 |
102 | // Lifecycle
103 | val lifecycleVersion = "2.4.1"
104 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
105 | implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
106 | implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
107 |
108 | // Navigation
109 | val navVersion = "2.4.1"
110 | implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
111 | implementation("androidx.navigation:navigation-compose:2.4.0-beta02")
112 |
113 | // Coroutine
114 | val coroutineVersion = "1.6.1"
115 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
116 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")
117 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutineVersion")
118 |
119 | // Timber
120 | implementation("com.jakewharton.timber:timber:5.0.1")
121 |
122 | // Retrofit
123 | implementation("com.squareup.retrofit2:retrofit:2.9.0")
124 |
125 | // OkHttp
126 | implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3"))
127 | implementation("com.squareup.okhttp3:okhttp")
128 | implementation("com.squareup.okhttp3:logging-interceptor")
129 |
130 | // DataStore
131 | implementation("androidx.datastore:datastore-preferences:1.0.0")
132 | implementation("androidx.datastore:datastore:1.0.0")
133 |
134 | // Kotlin Serialization
135 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
136 | implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
137 |
138 | // Gson
139 | implementation("com.google.code.gson:gson:2.9.0")
140 | implementation("com.squareup.retrofit2:converter-gson:2.9.0")
141 |
142 | // Room
143 | val roomVersion = "2.4.2"
144 | implementation("androidx.room:room-runtime:$roomVersion")
145 | kapt("androidx.room:room-compiler:$roomVersion")
146 | implementation("androidx.room:room-ktx:$roomVersion")
147 |
148 | // Test
149 | testImplementation("junit:junit:4.13.2")
150 | androidTestImplementation("androidx.test.ext:junit:1.1.3")
151 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
152 |
153 | val mockkVersion = "1.12.3"
154 | implementation("io.mockk:mockk:$mockkVersion")
155 | testImplementation("androidx.test.ext:junit-ktx:1.1.3")
156 |
157 |
158 | testImplementation("com.google.truth:truth:1.1.3")
159 | testImplementation("androidx.arch.core:core-testing:2.1.0")
160 | testImplementation("androidx.test:core-ktx:1.4.0")
161 | testImplementation("androidx.test:rules:1.4.0")
162 | testImplementation("androidx.test:runner:1.4.0")
163 | }
164 |
--------------------------------------------------------------------------------
/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.kts.
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
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/java/jp/dosukoi/githubclientforjetpackcompose/App.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclientforjetpackcompose
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 | import timber.log.Timber
6 |
7 | @HiltAndroidApp
8 | class App : Application() {
9 |
10 | override fun onCreate() {
11 | super.onCreate()
12 |
13 | Timber.plant(Timber.DebugTree())
14 | Timber.d("debug: onCreate")
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/jp/dosukoi/githubclientforjetpackcompose/AppModule.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclientforjetpackcompose
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.android.qualifiers.ApplicationContext
10 | import dagger.hilt.components.SingletonComponent
11 | import jp.dosukoi.data.api.common.AccessTokenProvider
12 | import jp.dosukoi.data.api.common.IApiType
13 | import jp.dosukoi.data.api.common.IAuthApiType
14 | import jp.dosukoi.data.repository.common.AppDatabase
15 | import jp.dosukoi.githubclient.domain.entity.auth.AuthDao
16 | import kotlinx.serialization.ExperimentalSerializationApi
17 | import kotlinx.serialization.json.Json
18 | import okhttp3.MediaType.Companion.toMediaType
19 | import okhttp3.OkHttpClient
20 | import okhttp3.logging.HttpLoggingInterceptor
21 | import retrofit2.Retrofit
22 | import javax.inject.Named
23 | import javax.inject.Singleton
24 |
25 | @InstallIn(SingletonComponent::class)
26 | @Module
27 | object AppModule {
28 |
29 | @ExperimentalSerializationApi
30 | @Provides
31 | @Singleton
32 | @Named("retrofit")
33 | fun provideRetrofit(
34 | okHttpClient: OkHttpClient
35 | ): Retrofit {
36 | val contentType = "application/json".toMediaType()
37 | val format = Json { ignoreUnknownKeys = true }
38 | return Retrofit.Builder()
39 | .baseUrl("https://api.github.com")
40 | .client(okHttpClient)
41 | .addConverterFactory(format.asConverterFactory(contentType))
42 | .build()
43 | }
44 |
45 | @ExperimentalSerializationApi
46 | @Provides
47 | @Singleton
48 | @Named("authRetrofit")
49 | fun provideAuthRetrofit(
50 | okHttpClient: OkHttpClient
51 | ): Retrofit {
52 | val contentType = "application/json".toMediaType()
53 | val format = Json { ignoreUnknownKeys = true }
54 | return Retrofit.Builder()
55 | .baseUrl("https://github.com")
56 | .client(okHttpClient)
57 | .addConverterFactory(format.asConverterFactory(contentType))
58 | .build()
59 | }
60 |
61 | @Provides
62 | @Singleton
63 | fun provideIApiType(
64 | @Named("retrofit") retrofit: Retrofit
65 | ): IApiType {
66 | return retrofit.create(IApiType::class.java)
67 | }
68 |
69 | @Provides
70 | @Singleton
71 | fun provideIAuthApiType(
72 | @Named("authRetrofit") retrofit: Retrofit
73 | ): IAuthApiType {
74 | return retrofit.create(IAuthApiType::class.java)
75 | }
76 |
77 | @Provides
78 | @Singleton
79 | fun provideOkHttpClient(
80 | accessTokenProvider: AccessTokenProvider
81 | ): OkHttpClient {
82 | return OkHttpClient.Builder()
83 | .addInterceptor(
84 | HttpLoggingInterceptor().apply {
85 | this.level = HttpLoggingInterceptor.Level.BODY
86 | }
87 | )
88 | .addInterceptor { chain ->
89 | val token = accessTokenProvider.provide()
90 | val request = chain.request().newBuilder()
91 | .header("Authorization", "token $token")
92 | .header("Accept", "application/json")
93 | .build()
94 | chain.proceed(request)
95 | }
96 | .build()
97 | }
98 |
99 | @Named("clientId")
100 | @Provides
101 | @Singleton
102 | fun provideClientId(): String {
103 | return "52b65f6025ea1e4264cd"
104 | }
105 |
106 | @Named("clientSecret")
107 | @Provides
108 | @Singleton
109 | fun provideClientSecret(): String {
110 | return "ac066b7d2508febfcb335faf63928379cb4dcda0"
111 | }
112 |
113 | @Provides
114 | @Singleton
115 | fun provideApplicationContext(
116 | @ApplicationContext context: Context
117 | ): Context {
118 | return context
119 | }
120 |
121 | @Provides
122 | @Singleton
123 | fun provideDatabase(
124 | @ApplicationContext context: Context
125 | ): AppDatabase {
126 | return Room.databaseBuilder(context, AppDatabase::class.java, "app_database").build()
127 | }
128 |
129 | @Provides
130 | @Singleton
131 | fun provideAuthDao(
132 | appDatabase: AppDatabase
133 | ): AuthDao {
134 | return appDatabase.authDao()
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/app/src/main/java/jp/dosukoi/githubclientforjetpackcompose/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclientforjetpackcompose
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import jp.dosukoi.data.api.common.IApiType
8 | import jp.dosukoi.data.api.common.IAuthApiType
9 | import jp.dosukoi.data.repository.auth.AuthRepositoryImpl
10 | import jp.dosukoi.data.repository.myPage.ReposRepositoryImpl
11 | import jp.dosukoi.data.repository.myPage.UserRepositoryImpl
12 | import jp.dosukoi.data.repository.search.SearchRepositoryImpl
13 | import jp.dosukoi.githubclient.domain.entity.auth.AuthDao
14 | import jp.dosukoi.githubclient.domain.repository.auth.AuthRepository
15 | import jp.dosukoi.githubclient.domain.repository.myPage.ReposRepository
16 | import jp.dosukoi.githubclient.domain.repository.myPage.UserRepository
17 | import jp.dosukoi.githubclient.domain.repository.search.SearchRepository
18 | import javax.inject.Named
19 | import javax.inject.Singleton
20 |
21 | @InstallIn(SingletonComponent::class)
22 | @Module
23 | object RepositoryModule {
24 | @Singleton
25 | @Provides
26 | fun provideAuthRepository(
27 | @Named("clientId") clientId: String,
28 | @Named("clientSecret") clientSecret: String,
29 | api: IAuthApiType,
30 | authDao: AuthDao
31 | ): AuthRepository = AuthRepositoryImpl(clientId, clientSecret, api, authDao)
32 |
33 | @Singleton
34 | @Provides
35 | fun provideReposRepository(api: IApiType): ReposRepository = ReposRepositoryImpl(api)
36 |
37 | @Singleton
38 | @Provides
39 | fun provideUserRepository(api: IApiType): UserRepository = UserRepositoryImpl(api)
40 |
41 | @Singleton
42 | @Provides
43 | fun provideSearchRepository(api: IApiType): SearchRepository = SearchRepositoryImpl(api)
44 | }
45 |
--------------------------------------------------------------------------------
/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/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/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.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | GitHubClientForJetpackCompose
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
23 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 | dependencies {
8 | val kotlinVersion = "1.5.10"
9 |
10 | classpath("com.android.tools.build:gradle:7.1.2")
11 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
12 | classpath("com.google.dagger:hilt-android-gradle-plugin:2.40.1")
13 |
14 | val navVersion = "2.4.1"
15 | classpath("androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion")
16 |
17 | classpath(kotlin("gradle-plugin", version = kotlinVersion))
18 | classpath(kotlin("serialization", version = kotlinVersion))
19 |
20 |
21 | // NOTE: Do not place your application dependencies here; they belong
22 | // in the individual module build.gradle.kts files
23 | }
24 | }
25 |
26 | task("clean") {
27 | delete(rootProject.buildDir)
28 | }
29 |
30 | subprojects {
31 | tasks.withType {
32 | kotlinOptions.jvmTarget = "1.8"
33 | kotlinOptions.freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn")
34 | }
35 | }
36 |
37 | allprojects {
38 | repositories {
39 | google()
40 | mavenCentral()
41 |
42 | maven("https://androidx.dev/snapshots/builds/7957905/artifacts/repository")
43 | }
44 |
45 | val ktlint by configurations.creating
46 | dependencies {
47 | ktlint("com.pinterest:ktlint:0.44.0") {
48 | attributes {
49 | attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
50 | }
51 | }
52 | }
53 |
54 | val outputDir = "${buildDir}/reports/ktlint/"
55 | val inputFiles = project.fileTree(mapOf("dir" to "src", "include" to "**/*.kt"))
56 |
57 | val ktlintCheck by tasks.creating(JavaExec::class) {
58 | inputs.files(inputFiles)
59 | outputs.dir(outputDir)
60 |
61 | description = "Check Kotlin code style."
62 | classpath = ktlint
63 | main = "com.pinterest.ktlint.Main"
64 | args = listOf("src/**/*.kt")
65 | }
66 |
67 | val ktlintFormat by tasks.creating(JavaExec::class) {
68 | inputs.files(inputFiles)
69 | outputs.dir(outputDir)
70 |
71 | description = "Fix Kotlin code style deviations."
72 | classpath = ktlint
73 | main = "com.pinterest.ktlint.Main"
74 | args = listOf("-F", "src/**/*.kt")
75 | }
76 | }
77 |
78 | val projectDependencyGraph by tasks.registering(ProjectDependencyGraphTask::class) {
79 | doLast {
80 | copy {
81 | from(rootProject.buildDir.resolve("reports/dependency-graph/project.dot.png"))
82 | into(rootProject.projectDir)
83 | }
84 | }
85 | }
86 |
87 |
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | repositories {
6 | google()
7 | mavenCentral()
8 | }
9 |
10 | dependencies {
11 | implementation("com.android.tools.build:gradle:7.0.3")
12 | implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10")
13 | }
14 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/Common.kt:
--------------------------------------------------------------------------------
1 | import com.android.build.gradle.LibraryExtension
2 | import org.gradle.api.JavaVersion
3 |
4 | fun LibraryExtension.applyCommon() {
5 | compileSdk = 31
6 | buildToolsVersion = "30.0.3"
7 |
8 | defaultConfig {
9 | minSdk = 26
10 | targetSdk = 31
11 |
12 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
13 | }
14 |
15 | buildTypes {
16 | getByName("release") {
17 | isMinifyEnabled = false
18 | proguardFiles(
19 | getDefaultProguardFile("proguard-android-optimize.txt"),
20 | "proguard-rules.pro"
21 | )
22 | }
23 | }
24 | compileOptions {
25 | sourceCompatibility = JavaVersion.VERSION_1_8
26 | targetCompatibility = JavaVersion.VERSION_1_8
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/ProjectDependencyGraphTask.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.DefaultTask
2 | import org.gradle.api.Project
3 | import org.gradle.api.artifacts.ProjectDependency
4 | import org.gradle.api.tasks.TaskAction
5 | import java.util.Locale
6 |
7 | @Suppress("unused")
8 | open class ProjectDependencyGraphTask : DefaultTask() {
9 | @TaskAction
10 | fun run() {
11 | val dot = project.rootDir.resolve("gradle/dependency-graph/project.dot")
12 | dot.parentFile.mkdirs()
13 | dot.delete()
14 |
15 | dot.appendText(
16 | """
17 | |digraph {
18 | | graph [label="${project.rootProject.name}\n ",labelloc=t,fontsize=30,ranksep=1.4];
19 | | node [style=filled, fillcolor="#bbbbbb"];
20 | | rankdir=TB;
21 | |
22 | """.trimMargin()
23 | )
24 |
25 | val rootProjects = mutableListOf()
26 | val queue = mutableListOf(project.rootProject)
27 | while (queue.isNotEmpty()) {
28 | val project = queue.removeAt(0)
29 | rootProjects.add(project)
30 | queue.addAll(project.childProjects.values)
31 | }
32 |
33 | val projects = LinkedHashSet()
34 | val dependencies = LinkedHashMap, MutableList>()
35 | val multiplatformProjects = mutableListOf()
36 | val jsProjects = mutableListOf()
37 | val androidProjects = mutableListOf()
38 | val javaProjects = mutableListOf()
39 | val rankAndroid = mutableListOf()
40 | val rankDomain = mutableListOf()
41 | val rankRepository = mutableListOf()
42 |
43 | queue.clear()
44 | queue.add(project.rootProject)
45 | while (queue.isNotEmpty()) {
46 | val project = queue.removeAt(0)
47 | queue.addAll(project.childProjects.values)
48 |
49 | if (project.plugins.hasPlugin("org.jetbrains.kotlin.multiplatform")) {
50 | multiplatformProjects.add(project)
51 | }
52 | if (project.plugins.hasPlugin("kotlin2js")) {
53 | jsProjects.add(project)
54 | }
55 | if (project.plugins.hasPlugin("com.android.library") || project.plugins.hasPlugin("com.android.application")) {
56 | androidProjects.add(project)
57 | }
58 | if (project.plugins.hasPlugin("java-library") || project.plugins.hasPlugin("java")) {
59 | javaProjects.add(project)
60 | }
61 |
62 | if (!project.path.startsWith(":core:") && project.path.endsWith(":android")) {
63 | rankAndroid.add(project)
64 | }
65 | if (!project.path.startsWith(":core:") && project.path.endsWith(":repository")) {
66 | rankRepository.add(project)
67 | }
68 |
69 | project.configurations.all {
70 | getDependencies()
71 | .filterIsInstance()
72 | .filter { project != it.dependencyProject }
73 | .map { it.dependencyProject }
74 | .forEach { dependency ->
75 | projects.add(project)
76 | projects.add(dependency)
77 | rootProjects.remove(dependency)
78 |
79 | val graphKey = Pair(project, dependency)
80 | val traits = dependencies.computeIfAbsent(graphKey) { mutableListOf() }
81 |
82 | if (name.toLowerCase(Locale.getDefault()).endsWith("implementation")) {
83 | traits.add("style=dotted")
84 | }
85 | }
86 | }
87 | }
88 |
89 | projects.sortedBy { it.path }.also {
90 | projects.clear()
91 | projects.addAll(it)
92 | }
93 |
94 | dot.appendText("\n # Projects\n\n")
95 | for (project in projects) {
96 | val traits = mutableListOf()
97 |
98 | if (rootProjects.contains(project)) {
99 | traits.add("shape=box")
100 | }
101 |
102 | if (multiplatformProjects.contains(project)) {
103 | if (androidProjects.contains(project)) {
104 | traits.add("fillcolor=\"#f7ffad\"")
105 | } else {
106 | traits.add("fillcolor=\"#ffd2b3\"")
107 | }
108 | } else if (jsProjects.contains(project)) {
109 | traits.add("fillcolor=\"#ffffba\"")
110 | } else if (androidProjects.contains(project)) {
111 | traits.add("fillcolor=\"#baffc9\"")
112 | } else if (javaProjects.contains(project)) {
113 | traits.add("fillcolor=\"#ffb3ba\"")
114 | } else {
115 | traits.add("fillcolor=\"#eeeeee\"")
116 | }
117 |
118 | dot.appendText(" \"${project.path}\" [${traits.joinToString(", ")}];\n")
119 | }
120 |
121 | dot.appendText("\n {rank = same;")
122 | for (project in projects) {
123 | if (rootProjects.contains(project)) {
124 | dot.appendText(" \"${project.path}\";")
125 | }
126 | }
127 | dot.appendText("}\n")
128 |
129 | for (sameRank in listOf(rankAndroid, rankDomain, rankRepository)) {
130 | dot.appendText("\n {rank = same;")
131 | for (project in sameRank) {
132 | dot.appendText(" \"${project.path}\";")
133 | }
134 | dot.appendText("}\n")
135 | }
136 |
137 | dot.appendText("\n # Dependencies\n\n")
138 | dependencies.forEach { (key, traits) ->
139 | dot.appendText(" \"${key.first.path}\" -> \"${key.second.path}\"")
140 | if (traits.isNotEmpty()) {
141 | dot.appendText(" [${traits.joinToString(", ")}]")
142 | }
143 | dot.appendText("\n")
144 | }
145 |
146 | dot.appendText("}\n")
147 |
148 | project.exec {
149 | commandLine = listOf("sh", "-c", "cd \"${dot.parentFile}\"; dot -Tpng -O project.dot")
150 | }
151 | println("Project module dependency graph created at ${dot.absolutePath}.png")
152 | }
153 | }
154 |
155 |
--------------------------------------------------------------------------------
/data/api/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/data/api/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | kotlin("kapt")
5 | id("dagger.hilt.android.plugin")
6 | id("kotlin-parcelize")
7 | }
8 |
9 | android {
10 | applyCommon()
11 | }
12 |
13 | dependencies {
14 |
15 | implementation(project(":domain:entity"))
16 |
17 | // Hilt
18 | val hiltVersion = "2.41"
19 | val hiltJetpackVersion = "1.0.0"
20 | implementation("com.google.dagger:hilt-android:$hiltVersion")
21 |
22 | kapt("com.google.dagger:hilt-android-compiler:$hiltVersion")
23 | kapt("androidx.hilt:hilt-compiler:$hiltJetpackVersion")
24 |
25 | // Coroutine
26 | val coroutineVersion = "1.6.1"
27 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
28 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")
29 |
30 | // Retrofit
31 | implementation("com.squareup.retrofit2:retrofit:2.9.0")
32 |
33 | // OkHttp
34 | implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3"))
35 |
36 | implementation("com.squareup.okhttp3:okhttp")
37 | implementation("com.squareup.okhttp3:logging-interceptor")
38 |
39 | // Timber
40 | implementation("com.jakewharton.timber:timber:5.0.1")
41 |
42 | testImplementation("junit:junit:4.13.2")
43 |
44 | testImplementation("com.google.truth:truth:1.1.3")
45 | }
46 |
--------------------------------------------------------------------------------
/data/api/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/data/api/consumer-rules.pro
--------------------------------------------------------------------------------
/data/api/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.kts.
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
--------------------------------------------------------------------------------
/data/api/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/data/api/src/main/java/jp/dosukoi/data/api/common/AccessTokenProvider.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.data.api.common
2 |
3 | import jp.dosukoi.githubclient.domain.entity.auth.AuthDao
4 | import javax.inject.Inject
5 | import javax.inject.Singleton
6 |
7 | @Singleton
8 | class AccessTokenProvider @Inject constructor(
9 | private val authDao: AuthDao
10 | ) {
11 |
12 | fun provide(): String? {
13 | return authDao.getAuth()?.accessToken
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/data/api/src/main/java/jp/dosukoi/data/api/common/IApiType.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.data.api.common
2 |
3 | import jp.dosukoi.githubclient.domain.entity.myPage.Repository
4 | import jp.dosukoi.githubclient.domain.entity.myPage.User
5 | import jp.dosukoi.githubclient.domain.entity.search.Search
6 | import retrofit2.Response
7 | import retrofit2.http.GET
8 | import retrofit2.http.Query
9 |
10 | interface IApiType {
11 |
12 | @GET("/user")
13 | suspend fun getUser(): Response
14 |
15 | @GET("/user/repos")
16 | suspend fun getMyRepositoryList(
17 | @Query("sort") sort: String = "updated"
18 | ): Response>
19 |
20 | @GET("/search/repositories")
21 | suspend fun findRepositories(
22 | @Query("q") query: String?,
23 | @Query("page") page: Int,
24 | @Query("sort") sort: String = "updated",
25 | @Query("per_page") perPage: Int? = null
26 | ): Response
27 | }
28 |
--------------------------------------------------------------------------------
/data/api/src/main/java/jp/dosukoi/data/api/common/IAuthApiType.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.data.api.common
2 |
3 | import jp.dosukoi.githubclient.domain.entity.auth.Auth
4 | import retrofit2.Response
5 | import retrofit2.http.POST
6 | import retrofit2.http.Query
7 |
8 | interface IAuthApiType {
9 |
10 | @POST("/login/oauth/access_token")
11 | suspend fun getAccessToken(
12 | @Query("client_id") clientId: String,
13 | @Query("client_secret") clientSecret: String,
14 | @Query("code") code: String
15 | ): Response
16 | }
17 |
--------------------------------------------------------------------------------
/data/repository/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/data/repository/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | kotlin("kapt")
5 | id("dagger.hilt.android.plugin")
6 | id("kotlin-parcelize")
7 | }
8 |
9 | android {
10 | applyCommon()
11 | }
12 |
13 | dependencies {
14 |
15 | implementation(project(":data:api"))
16 | implementation(project(":domain:repository"))
17 |
18 | // Hilt
19 | val hiltVersion = "2.41"
20 | val hiltJetpackVersion = "1.0.0"
21 | implementation("com.google.dagger:hilt-android:$hiltVersion")
22 |
23 | kapt("com.google.dagger:hilt-android-compiler:$hiltVersion")
24 | kapt("androidx.hilt:hilt-compiler:$hiltJetpackVersion")
25 |
26 | // Coroutine
27 | val coroutineVersion = "1.6.1"
28 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
29 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")
30 |
31 | // Retrofit
32 | implementation("com.squareup.retrofit2:retrofit:2.9.0")
33 |
34 | // OkHttp
35 | implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3"))
36 |
37 | implementation("com.squareup.okhttp3:okhttp")
38 | implementation("com.squareup.okhttp3:logging-interceptor")
39 |
40 | // Timber
41 | implementation("com.jakewharton.timber:timber:5.0.1")
42 |
43 | // Lifecycle
44 | val lifecycleVersion = "2.4.1"
45 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
46 | implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
47 |
48 | // Room
49 | val roomVersion = "2.4.2"
50 | implementation("androidx.room:room-runtime:$roomVersion")
51 | kapt("androidx.room:room-compiler:$roomVersion")
52 | implementation("androidx.room:room-ktx:$roomVersion")
53 |
54 | testImplementation("junit:junit:4.13.2")
55 | testImplementation("com.google.truth:truth:1.1.3")
56 | }
57 |
--------------------------------------------------------------------------------
/data/repository/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/data/repository/consumer-rules.pro
--------------------------------------------------------------------------------
/data/repository/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.kts.
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
--------------------------------------------------------------------------------
/data/repository/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/data/repository/src/main/java/jp/dosukoi/data/repository/auth/AuthRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.data.repository.auth
2 |
3 | import dagger.Reusable
4 | import jp.dosukoi.data.api.common.IAuthApiType
5 | import jp.dosukoi.data.repository.common.asyncFetch
6 | import jp.dosukoi.githubclient.domain.entity.auth.AuthDao
7 | import jp.dosukoi.githubclient.domain.entity.auth.AuthEntity
8 | import jp.dosukoi.githubclient.domain.repository.auth.AuthRepository
9 | import javax.inject.Inject
10 | import javax.inject.Named
11 |
12 | @Reusable
13 | class AuthRepositoryImpl @Inject constructor(
14 | @Named("clientId") private val clientId: String,
15 | @Named("clientSecret") private val clientSecret: String,
16 | private val api: IAuthApiType,
17 | private val authDao: AuthDao
18 | ) : AuthRepository {
19 |
20 | override suspend fun getAccessToken(code: String) {
21 | asyncFetch({ api.getAccessToken(clientId, clientSecret, code) }) {
22 | authDao.insert(AuthEntity(0, it.accessToken))
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/data/repository/src/main/java/jp/dosukoi/data/repository/common/ApiTransaction.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.data.repository.common
2 |
3 | import retrofit2.HttpException
4 | import retrofit2.Response
5 |
6 | suspend fun asyncFetch(
7 | apiFunction: suspend () -> Response,
8 | onSuccess: suspend (T) -> Unit
9 | ) {
10 | try {
11 | val response = apiFunction.invoke()
12 | if (response.isSuccessful) {
13 | onSuccess(response.body()!!)
14 | } else throw HttpException(response)
15 | } catch (throwable: Throwable) {
16 | throw throwable
17 | }
18 | }
19 |
20 | suspend fun asyncFetch(
21 | apiFunction: suspend () -> Response,
22 | ): T {
23 | return try {
24 | val response = apiFunction.invoke()
25 | if (response.isSuccessful) {
26 | response.body()!!
27 | } else throw HttpException(response)
28 | } catch (throwable: Throwable) {
29 | throw throwable
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/data/repository/src/main/java/jp/dosukoi/data/repository/common/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.data.repository.common
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import jp.dosukoi.githubclient.domain.entity.auth.AuthDao
6 | import jp.dosukoi.githubclient.domain.entity.auth.AuthEntity
7 | import javax.inject.Singleton
8 |
9 | @Singleton
10 | @Database(entities = [AuthEntity::class], version = 1)
11 | abstract class AppDatabase : RoomDatabase() {
12 | abstract fun authDao(): AuthDao
13 | }
14 |
--------------------------------------------------------------------------------
/data/repository/src/main/java/jp/dosukoi/data/repository/common/LiveDataExt.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.data.repository.common
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MediatorLiveData
5 | import androidx.lifecycle.distinctUntilChanged
6 |
7 | fun zip(
8 | liveData1: LiveData,
9 | liveData2: LiveData,
10 | initialValue: C? = null,
11 | operator: (T, R) -> C
12 | ): LiveData {
13 | return MediatorLiveData().apply {
14 | value = initialValue
15 | listOf(
16 | liveData1,
17 | liveData2
18 | ).forEach { liveData ->
19 | addSource(liveData) {
20 | val liveData1Value = liveData1.value
21 | val liveData2Value = liveData2.value
22 | if (liveData1Value != null && liveData2Value != null) {
23 | value = operator(liveData1Value, liveData2Value)
24 | }
25 | }
26 | }
27 | }.distinctUntilChanged()
28 | }
29 |
--------------------------------------------------------------------------------
/data/repository/src/main/java/jp/dosukoi/data/repository/myPage/ReposRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.data.repository.myPage
2 |
3 | import jp.dosukoi.data.api.common.IApiType
4 | import jp.dosukoi.data.repository.common.asyncFetch
5 | import jp.dosukoi.githubclient.domain.entity.auth.UnAuthorizeException
6 | import jp.dosukoi.githubclient.domain.entity.myPage.Repository
7 | import jp.dosukoi.githubclient.domain.repository.myPage.ReposRepository
8 | import retrofit2.HttpException
9 | import javax.inject.Inject
10 | import javax.inject.Singleton
11 |
12 | @Singleton
13 | class ReposRepositoryImpl @Inject constructor(
14 | private val api: IApiType
15 | ) : ReposRepository {
16 |
17 | override suspend fun getRepositoryList(): List {
18 | return try {
19 | asyncFetch { api.getMyRepositoryList() }
20 | } catch (throwable: Throwable) {
21 | if (throwable is HttpException) {
22 | when (throwable.code()) {
23 | 401, 403 -> throw UnAuthorizeException(throwable.message())
24 | else -> throw throwable
25 | }
26 | } else {
27 | throw throwable
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/data/repository/src/main/java/jp/dosukoi/data/repository/myPage/UserRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.data.repository.myPage
2 |
3 | import jp.dosukoi.data.api.common.IApiType
4 | import jp.dosukoi.data.repository.common.asyncFetch
5 | import jp.dosukoi.githubclient.domain.entity.auth.UnAuthorizeException
6 | import jp.dosukoi.githubclient.domain.entity.myPage.UserStatus
7 | import jp.dosukoi.githubclient.domain.repository.myPage.UserRepository
8 | import retrofit2.HttpException
9 | import javax.inject.Inject
10 | import javax.inject.Singleton
11 |
12 | @Singleton
13 | class UserRepositoryImpl @Inject constructor(
14 | private val api: IApiType
15 | ) : UserRepository {
16 |
17 | override suspend fun getUser(): UserStatus {
18 | return try {
19 | UserStatus.Authenticated(asyncFetch { api.getUser() })
20 | } catch (throwable: Throwable) {
21 | if (throwable is HttpException) {
22 | when (throwable.code()) {
23 | 401, 403 -> throw UnAuthorizeException(throwable.message())
24 | else -> throw throwable
25 | }
26 | } else {
27 | throw throwable
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/data/repository/src/main/java/jp/dosukoi/data/repository/search/SearchRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.data.repository.search
2 |
3 | import jp.dosukoi.data.api.common.IApiType
4 | import jp.dosukoi.data.repository.common.asyncFetch
5 | import jp.dosukoi.githubclient.domain.entity.search.Search
6 | import jp.dosukoi.githubclient.domain.repository.search.SearchRepository
7 | import javax.inject.Inject
8 | import javax.inject.Singleton
9 |
10 | @Singleton
11 | class SearchRepositoryImpl @Inject constructor(
12 | private val api: IApiType
13 | ) : SearchRepository {
14 |
15 | override suspend fun findRepositories(
16 | query: String?,
17 | page: Int,
18 | ): Search {
19 | return asyncFetch { api.findRepositories(query, page) }
20 | }
21 |
22 | companion object {
23 | private const val PER_PAGE = 30
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/data/usecase/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/data/usecase/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | kotlin("kapt")
5 | id("dagger.hilt.android.plugin")
6 | id("kotlin-parcelize")
7 | }
8 |
9 | android {
10 | applyCommon()
11 | }
12 |
13 | dependencies {
14 |
15 | api(project(":data:repository"))
16 | testImplementation(project(":testing"))
17 |
18 | // Hilt
19 | val hiltVersion = "2.41"
20 | val hiltJetpackVersion = "1.0.0"
21 | implementation("com.google.dagger:hilt-android:$hiltVersion")
22 |
23 | kapt("com.google.dagger:hilt-android-compiler:$hiltVersion")
24 | kapt("androidx.hilt:hilt-compiler:$hiltJetpackVersion")
25 |
26 | // Coroutine
27 | val coroutineVersion = "1.6.0"
28 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
29 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")
30 | testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutineVersion")
31 |
32 | // Retrofit
33 | implementation("com.squareup.retrofit2:retrofit:2.9.0")
34 |
35 | // OkHttp
36 | implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3"))
37 |
38 | implementation("com.squareup.okhttp3:okhttp")
39 | implementation("com.squareup.okhttp3:logging-interceptor")
40 |
41 | // Timber
42 | implementation("com.jakewharton.timber:timber:5.0.1")
43 |
44 | // Lifecycle
45 | val lifecycleVersion = "2.4.1"
46 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
47 | implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
48 |
49 | // Room
50 | val roomVersion = "2.4.2"
51 | implementation("androidx.room:room-runtime:$roomVersion")
52 | kapt("androidx.room:room-compiler:$roomVersion")
53 | implementation("androidx.room:room-ktx:$roomVersion")
54 |
55 | // Test
56 | val mockkVersion = "1.12.2"
57 | implementation("io.mockk:mockk:$mockkVersion")
58 | testImplementation("androidx.test.ext:junit-ktx:1.1.3")
59 |
60 |
61 | testImplementation("junit:junit:4.13.2")
62 | testImplementation("com.google.truth:truth:1.1.3")
63 | testImplementation("androidx.arch.core:core-testing:2.1.0")
64 | testImplementation("androidx.test:core-ktx:1.4.0")
65 | testImplementation("androidx.test:rules:1.4.0")
66 | testImplementation("androidx.test:runner:1.4.0")
67 | }
68 |
--------------------------------------------------------------------------------
/data/usecase/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/data/usecase/consumer-rules.pro
--------------------------------------------------------------------------------
/data/usecase/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.kts.
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
--------------------------------------------------------------------------------
/data/usecase/src/androidTest/java/jp/dosukoi/data/usecase/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.data.usecase
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("jp.dosukoi.data.usecase.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/data/usecase/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/data/usecase/src/test/java/jp/dosukoi/data/usecase/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.data.usecase
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/domain/entity/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/domain/entity/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | kotlin("kapt")
5 | id("dagger.hilt.android.plugin")
6 | id("kotlin-parcelize")
7 | kotlin("plugin.serialization")
8 | }
9 |
10 | android {
11 | applyCommon()
12 | }
13 |
14 | dependencies {
15 |
16 | // Hilt
17 | val hiltVersion = "2.41"
18 | val hiltJetpackVersion = "1.0.0"
19 | implementation("com.google.dagger:hilt-android:$hiltVersion")
20 |
21 | kapt("com.google.dagger:hilt-android-compiler:$hiltVersion")
22 | kapt("androidx.hilt:hilt-compiler:$hiltJetpackVersion")
23 |
24 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
25 |
26 | // Kotlin Serialization Converter
27 | implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
28 |
29 | // Gson
30 | implementation("com.google.code.gson:gson:2.9.0")
31 | implementation("com.squareup.retrofit2:converter-gson:2.9.0")
32 |
33 | // Room
34 | val roomVersion = "2.4.2"
35 | implementation("androidx.room:room-runtime:$roomVersion")
36 | kapt("androidx.room:room-compiler:$roomVersion")
37 | implementation("androidx.room:room-ktx:$roomVersion")
38 |
39 | // Timber
40 | implementation("com.jakewharton.timber:timber:5.0.1")
41 |
42 | testImplementation("junit:junit:4.13.2")
43 | testImplementation("com.google.truth:truth:1.1.3")
44 | }
45 |
--------------------------------------------------------------------------------
/domain/entity/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/domain/entity/consumer-rules.pro
--------------------------------------------------------------------------------
/domain/entity/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
--------------------------------------------------------------------------------
/domain/entity/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/domain/entity/src/main/java/jp/dosukoi/githubclient/domain/entity/auth/Auth.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclient.domain.entity.auth
2 |
3 | import android.os.Parcelable
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 | import kotlinx.parcelize.Parcelize
7 | import kotlinx.serialization.SerialName
8 | import kotlinx.serialization.Serializable
9 |
10 | @Serializable
11 | @Parcelize
12 | data class Auth(
13 | @SerialName("access_token")
14 | val accessToken: String
15 | ) : Parcelable
16 |
17 | @Entity
18 | data class AuthEntity(
19 | @PrimaryKey
20 | val id: Long,
21 | val accessToken: String
22 | )
23 |
--------------------------------------------------------------------------------
/domain/entity/src/main/java/jp/dosukoi/githubclient/domain/entity/auth/AuthDao.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclient.domain.entity.auth
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 |
8 | @Dao
9 | interface AuthDao {
10 | @Insert(onConflict = OnConflictStrategy.REPLACE)
11 | suspend fun insert(auth: AuthEntity)
12 |
13 | @Query("SELECT * FROM AuthEntity WHERE id = 0")
14 | fun getAuth(): Auth?
15 | }
16 |
--------------------------------------------------------------------------------
/domain/entity/src/main/java/jp/dosukoi/githubclient/domain/entity/auth/UnAuthorizeException.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclient.domain.entity.auth
2 |
3 | class UnAuthorizeException(val errorMessage: String) : RuntimeException(errorMessage)
4 |
--------------------------------------------------------------------------------
/domain/entity/src/main/java/jp/dosukoi/githubclient/domain/entity/myPage/Repository.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclient.domain.entity.myPage
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 | import kotlinx.serialization.SerialName
6 | import kotlinx.serialization.Serializable
7 |
8 | @Serializable
9 | @Parcelize
10 | data class Repository(
11 | val id: Long,
12 | @SerialName("full_name")
13 | val fullName: String,
14 | val description: String?,
15 | @SerialName("html_url")
16 | val htmlUrl: String
17 | ) : Parcelable
18 |
--------------------------------------------------------------------------------
/domain/entity/src/main/java/jp/dosukoi/githubclient/domain/entity/myPage/User.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclient.domain.entity.myPage
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 | import kotlinx.serialization.SerialName
6 | import kotlinx.serialization.Serializable
7 |
8 | @Serializable
9 | @Parcelize
10 | data class User(
11 | val login: String,
12 | val id: Long,
13 | @SerialName("avatar_url")
14 | val avatarUrl: String,
15 | val url: String,
16 | @SerialName("html_url")
17 | val htmlUrl: String,
18 | val company: String?,
19 | val email: String?,
20 | val bio: String?
21 | ) : Parcelable
22 |
--------------------------------------------------------------------------------
/domain/entity/src/main/java/jp/dosukoi/githubclient/domain/entity/myPage/UserStatus.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclient.domain.entity.myPage
2 |
3 | sealed class UserStatus {
4 | class Authenticated(val user: User) : UserStatus()
5 | object UnAuthenticated : UserStatus()
6 | }
7 |
--------------------------------------------------------------------------------
/domain/entity/src/main/java/jp/dosukoi/githubclient/domain/entity/search/Search.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclient.domain.entity.search
2 |
3 | import android.os.Parcelable
4 | import jp.dosukoi.githubclient.domain.entity.myPage.Repository
5 | import kotlinx.parcelize.Parcelize
6 | import kotlinx.serialization.SerialName
7 | import kotlinx.serialization.Serializable
8 |
9 | @Serializable
10 | @Parcelize
11 | data class Search(
12 | @SerialName("total_count")
13 | val totalCount: Int,
14 | @SerialName("incomplete_results")
15 | val incompleteResults: Boolean,
16 | val items: List
17 | ) : Parcelable
18 |
--------------------------------------------------------------------------------
/domain/repository/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/domain/repository/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | kotlin("kapt")
5 | id("dagger.hilt.android.plugin")
6 | id("kotlin-parcelize")
7 | }
8 |
9 | android {
10 | applyCommon()
11 | }
12 |
13 | dependencies {
14 | api(project(":domain:entity"))
15 |
16 | // Hilt
17 | val hiltVersion = "2.41"
18 | val hiltJetpackVersion = "1.0.0"
19 | implementation("com.google.dagger:hilt-android:$hiltVersion")
20 |
21 | kapt("com.google.dagger:hilt-android-compiler:$hiltVersion")
22 | kapt("androidx.hilt:hilt-compiler:$hiltJetpackVersion")
23 |
24 | // Coroutine
25 | val coroutineVersion = "1.6.1"
26 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
27 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")
28 | }
29 |
--------------------------------------------------------------------------------
/domain/repository/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/domain/repository/consumer-rules.pro
--------------------------------------------------------------------------------
/domain/repository/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
--------------------------------------------------------------------------------
/domain/repository/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/domain/repository/src/main/java/jp/dosukoi/githubclient/domain/repository/auth/AuthRepository.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclient.domain.repository.auth
2 |
3 | interface AuthRepository {
4 | suspend fun getAccessToken(code: String)
5 | }
6 |
--------------------------------------------------------------------------------
/domain/repository/src/main/java/jp/dosukoi/githubclient/domain/repository/myPage/ReposRepository.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclient.domain.repository.myPage
2 |
3 | import jp.dosukoi.githubclient.domain.entity.myPage.Repository
4 |
5 | interface ReposRepository {
6 | suspend fun getRepositoryList(): List
7 | }
8 |
--------------------------------------------------------------------------------
/domain/repository/src/main/java/jp/dosukoi/githubclient/domain/repository/myPage/UserRepository.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclient.domain.repository.myPage
2 |
3 | import jp.dosukoi.githubclient.domain.entity.myPage.UserStatus
4 |
5 | interface UserRepository {
6 | suspend fun getUser(): UserStatus
7 | }
8 |
--------------------------------------------------------------------------------
/domain/repository/src/main/java/jp/dosukoi/githubclient/domain/repository/search/SearchRepository.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclient.domain.repository.search
2 |
3 | import jp.dosukoi.githubclient.domain.entity.search.Search
4 |
5 | interface SearchRepository {
6 | suspend fun findRepositories(query: String?, page: Int): Search
7 | }
8 |
--------------------------------------------------------------------------------
/domain/usecase/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/domain/usecase/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | kotlin("kapt")
5 | id("dagger.hilt.android.plugin")
6 | id("kotlin-parcelize")
7 | }
8 |
9 | android {
10 | applyCommon()
11 | }
12 |
13 | dependencies {
14 |
15 | implementation(project(":domain:repository"))
16 |
17 | // Hilt
18 | val hiltVersion = "2.41"
19 | val hiltJetpackVersion = "1.0.0"
20 | implementation("com.google.dagger:hilt-android:$hiltVersion")
21 |
22 | kapt("com.google.dagger:hilt-android-compiler:$hiltVersion")
23 | kapt("androidx.hilt:hilt-compiler:$hiltJetpackVersion")
24 |
25 | // Coroutine
26 | val coroutineVersion = "1.6.1"
27 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
28 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")
29 | }
30 |
--------------------------------------------------------------------------------
/domain/usecase/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/domain/usecase/consumer-rules.pro
--------------------------------------------------------------------------------
/domain/usecase/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
--------------------------------------------------------------------------------
/domain/usecase/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/jp/dosukoi/githubclient/domain/usecase/auth/GetAccessTokenUseCase.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclient.domain.usecase.auth
2 |
3 | import dagger.Reusable
4 | import jp.dosukoi.githubclient.domain.repository.auth.AuthRepository
5 | import javax.inject.Inject
6 |
7 | @Reusable
8 | class GetAccessTokenUseCase @Inject constructor(
9 | private val authRepository: AuthRepository
10 | ) {
11 |
12 | suspend fun execute(code: String) {
13 | authRepository.getAccessToken(code)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/jp/dosukoi/githubclient/domain/usecase/myPage/GetRepositoriesUseCase.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclient.domain.usecase.myPage
2 |
3 | import jp.dosukoi.githubclient.domain.entity.myPage.Repository
4 | import jp.dosukoi.githubclient.domain.repository.myPage.ReposRepository
5 | import javax.inject.Inject
6 | import javax.inject.Singleton
7 |
8 | @Singleton
9 | class GetRepositoriesUseCase @Inject constructor(
10 | private val reposRepository: ReposRepository
11 | ) {
12 | suspend fun execute(): List = reposRepository.getRepositoryList()
13 | }
14 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/jp/dosukoi/githubclient/domain/usecase/myPage/GetUserStatusUseCase.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclient.domain.usecase.myPage
2 |
3 | import jp.dosukoi.githubclient.domain.entity.myPage.UserStatus
4 | import jp.dosukoi.githubclient.domain.repository.myPage.UserRepository
5 | import javax.inject.Inject
6 | import javax.inject.Singleton
7 |
8 | @Singleton
9 | class GetUserStatusUseCase @Inject constructor(
10 | private val userRepository: UserRepository,
11 | ) {
12 |
13 | suspend fun execute(): UserStatus = userRepository.getUser()
14 | }
15 |
--------------------------------------------------------------------------------
/domain/usecase/src/main/java/jp/dosukoi/githubclient/domain/usecase/search/GetSearchDataUseCase.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.githubclient.domain.usecase.search
2 |
3 | import jp.dosukoi.githubclient.domain.entity.search.Search
4 | import jp.dosukoi.githubclient.domain.repository.search.SearchRepository
5 | import javax.inject.Inject
6 | import javax.inject.Singleton
7 |
8 | @Singleton
9 | class GetSearchDataUseCase @Inject constructor(
10 | private val searchRepository: SearchRepository
11 | ) {
12 |
13 | suspend fun execute(query: String?, page: Int): Search {
14 | return searchRepository.findRepositories(query, page)
15 | }
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=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError -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.caching=true
14 | org.gradle.configureondemand=false
15 | org.gradle.daemon=true
16 | org.gradle.parallel=true
17 | android.useAndroidX=true
18 |
--------------------------------------------------------------------------------
/gradle/dependency-graph/project.dot:
--------------------------------------------------------------------------------
1 | digraph {
2 | graph [label="GitHubClientForJetpackCompose\n ",labelloc=t,fontsize=30,ranksep=1.4];
3 | node [style=filled, fillcolor="#bbbbbb"];
4 | rankdir=TB;
5 |
6 | # Projects
7 |
8 | ":app" [shape=box, fillcolor="#baffc9"];
9 | ":data:api" [fillcolor="#baffc9"];
10 | ":data:repository" [fillcolor="#baffc9"];
11 | ":domain:entity" [fillcolor="#baffc9"];
12 | ":domain:repository" [fillcolor="#baffc9"];
13 | ":domain:usecase" [fillcolor="#baffc9"];
14 | ":testing" [fillcolor="#baffc9"];
15 | ":ui:view" [fillcolor="#baffc9"];
16 | ":ui:viewModel" [fillcolor="#baffc9"];
17 |
18 | {rank = same; ":app";}
19 |
20 | {rank = same;}
21 |
22 | {rank = same;}
23 |
24 | {rank = same; ":data:repository"; ":domain:repository";}
25 |
26 | # Dependencies
27 |
28 | ":app" -> ":ui:view" [style=dotted]
29 | ":app" -> ":ui:viewModel" [style=dotted]
30 | ":app" -> ":data:api" [style=dotted]
31 | ":app" -> ":data:repository" [style=dotted]
32 | ":app" -> ":domain:usecase" [style=dotted]
33 | ":app" -> ":domain:entity" [style=dotted]
34 | ":app" -> ":domain:repository" [style=dotted]
35 | ":data:api" -> ":domain:entity" [style=dotted]
36 | ":data:repository" -> ":data:api" [style=dotted]
37 | ":data:repository" -> ":domain:repository" [style=dotted]
38 | ":domain:repository" -> ":domain:entity"
39 | ":domain:usecase" -> ":domain:repository" [style=dotted]
40 | ":ui:view" -> ":ui:viewModel" [style=dotted]
41 | ":ui:view" -> ":domain:entity" [style=dotted]
42 | ":ui:viewModel" -> ":domain:entity" [style=dotted]
43 | ":ui:viewModel" -> ":domain:usecase" [style=dotted]
44 | ":ui:viewModel" -> ":testing" [style=dotted]
45 | }
46 |
--------------------------------------------------------------------------------
/gradle/dependency-graph/project.dot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/gradle/dependency-graph/project.dot.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Jul 27 13:12:21 JST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "GitHubClientForJetpackCompose"
2 | include(
3 | ":app",
4 | ":ui",
5 | ":ui:view",
6 | ":ui:viewModel",
7 | ":data:repository",
8 | ":data:api",
9 | ":domain:entity",
10 | ":domain:usecase",
11 | ":domain:repository",
12 | ":testing"
13 | )
14 |
--------------------------------------------------------------------------------
/testing/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/testing/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | kotlin("kapt")
5 | id("dagger.hilt.android.plugin")
6 | id("kotlin-parcelize")
7 | }
8 |
9 | android {
10 | applyCommon()
11 | }
12 |
13 | dependencies {
14 |
15 | // Hilt
16 | val hiltVersion = "2.41"
17 | val hiltJetpackVersion = "1.0.0"
18 | implementation("com.google.dagger:hilt-android:$hiltVersion")
19 |
20 | kapt("com.google.dagger:hilt-android-compiler:$hiltVersion")
21 | kapt("androidx.hilt:hilt-compiler:$hiltJetpackVersion")
22 |
23 | // Lifecycle
24 | val lifecycleVersion = "2.4.1"
25 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
26 | implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
27 |
28 | // Coroutine
29 | val coroutineVersion = "1.6.1"
30 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
31 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")
32 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutineVersion")
33 |
34 | // Timber
35 | implementation("com.jakewharton.timber:timber:5.0.1")
36 |
37 | // Serialization
38 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
39 |
40 | // Test
41 | val mockkVersion = "1.12.3"
42 | implementation("io.mockk:mockk:$mockkVersion")
43 | implementation("androidx.test.ext:junit-ktx:1.1.3")
44 |
45 |
46 | implementation("junit:junit:4.13.2")
47 | implementation("com.google.truth:truth:1.1.3")
48 | implementation("androidx.arch.core:core-testing:2.1.0")
49 | implementation("androidx.test:core-ktx:1.4.0")
50 | implementation("androidx.test:rules:1.4.0")
51 | implementation("androidx.test:runner:1.4.0")
52 | }
53 |
--------------------------------------------------------------------------------
/testing/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/testing/consumer-rules.pro
--------------------------------------------------------------------------------
/testing/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.kts.
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 |
--------------------------------------------------------------------------------
/testing/src/androidTest/java/jp/dosukoi/testing/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.testing
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("jp.dosukoi.testing.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/testing/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/testing/src/main/java/jp/dosukoi/testing/common/Assertion.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.testing.common
2 |
3 | import com.google.common.truth.Truth
4 |
5 | inline fun assertType(target: Any?, block: T.() -> Unit) {
6 | if (target is T) {
7 | target.block()
8 | } else {
9 | Truth.assertThat(target).isInstanceOf(T::class.java)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/testing/src/main/java/jp/dosukoi/testing/common/MainCoroutineRule.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.testing.common
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.test.TestCoroutineDispatcher
7 | import kotlinx.coroutines.test.resetMain
8 | import kotlinx.coroutines.test.setMain
9 | import org.junit.rules.RuleChain
10 | import org.junit.rules.TestRule
11 | import org.junit.rules.TestWatcher
12 | import org.junit.runner.Description
13 |
14 | fun testRule(): TestRule = RuleChain.outerRule(InstantTaskExecutorRule())
15 | .around(MainCoroutineRule())
16 |
17 | @OptIn(ExperimentalCoroutinesApi::class)
18 | class MainCoroutineRule(
19 | private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
20 | ) : TestWatcher() {
21 | override fun starting(description: Description?) {
22 | super.starting(description)
23 | Dispatchers.setMain(testDispatcher)
24 | }
25 |
26 | override fun finished(description: Description?) {
27 | super.finished(description)
28 | testDispatcher.scheduler.advanceUntilIdle()
29 | Dispatchers.resetMain()
30 | testDispatcher.cleanupTestCoroutines()
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/testing/src/main/java/jp/dosukoi/testing/common/TestLiveData.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.testing.common
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.Observer
5 |
6 | class TestLiveData(private val liveData: LiveData) {
7 | private val values = mutableListOf()
8 | private val observer = Observer { values.add(it) }
9 |
10 | init {
11 | liveData.observeForever(observer)
12 | }
13 |
14 | fun values(): MutableList {
15 | shutdown()
16 | return values
17 | }
18 |
19 | fun lastValue(): T {
20 | shutdown()
21 | return values.last()
22 | }
23 |
24 | fun complete() = shutdown()
25 |
26 | private fun shutdown() {
27 | if (liveData.hasActiveObservers()) {
28 | liveData.removeObserver(observer)
29 | }
30 | }
31 | }
32 |
33 | fun LiveData.test() = TestLiveData(this)
34 |
--------------------------------------------------------------------------------
/testing/src/test/java/jp/dosukoi/testing/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.testing
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/ui/view/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/ui/view/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | kotlin("kapt")
5 | id("dagger.hilt.android.plugin")
6 | id("androidx.navigation.safeargs.kotlin")
7 | id("kotlin-parcelize")
8 | }
9 |
10 | android {
11 | applyCommon()
12 |
13 | buildFeatures {
14 | compose = true
15 | dataBinding = true
16 | }
17 | composeOptions {
18 | kotlinCompilerExtensionVersion = "1.0.0"
19 | }
20 | kotlinOptions {
21 | jvmTarget = "1.8"
22 | }
23 | }
24 |
25 | dependencies {
26 |
27 | implementation(project(":ui:viewModel"))
28 | implementation(project(":domain:entity"))
29 |
30 | implementation("androidx.core:core-ktx:1.7.0")
31 | implementation("androidx.appcompat:appcompat:1.4.1")
32 | implementation("com.google.android.material:material:1.5.0")
33 |
34 | // Compose
35 | val composeVersion = "1.1.1"
36 | implementation("androidx.compose.ui:ui:$composeVersion")
37 | implementation("androidx.compose.runtime:runtime:$composeVersion")
38 | implementation("androidx.compose.runtime:runtime-livedata:$composeVersion")
39 | implementation("androidx.compose.material:material:$composeVersion")
40 | implementation("androidx.compose.foundation:foundation:$composeVersion")
41 | implementation("androidx.compose.compiler:compiler:$composeVersion")
42 | implementation("androidx.compose.animation:animation:$composeVersion")
43 | implementation("androidx.compose.foundation:foundation-layout:$composeVersion")
44 | implementation("androidx.compose.ui:ui-tooling:$composeVersion")
45 | implementation("androidx.compose.ui:ui-util:$composeVersion")
46 | implementation("io.coil-kt:coil-compose:1.4.0")
47 |
48 | // Compose Accompanist
49 | val accompanistVersion = "0.23.1"
50 | implementation("com.google.accompanist:accompanist-pager:$accompanistVersion")
51 | implementation("com.google.accompanist:accompanist-swiperefresh:$accompanistVersion")
52 |
53 | // Glance
54 | val glanceVersion = "1.0.0-SNAPSHOT"
55 | implementation("androidx.glance:glance:$glanceVersion")
56 | implementation("androidx.glance:glance-appwidget:$glanceVersion")
57 | implementation("androidx.glance:glance-appwidget-proto:$glanceVersion")
58 |
59 | // Lottie
60 | implementation("com.airbnb.android:lottie-compose:5.0.3")
61 |
62 | // DataStore
63 | implementation("androidx.datastore:datastore:1.0.0")
64 | implementation("androidx.datastore:datastore-preferences:1.0.0")
65 |
66 | // Hilt
67 | val hiltVersion = "2.41"
68 | val hiltJetpackVersion = "1.0.0"
69 | implementation("com.google.dagger:hilt-android:$hiltVersion")
70 | implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
71 | kapt("com.google.dagger:hilt-android-compiler:$hiltVersion")
72 | kapt("androidx.hilt:hilt-compiler:$hiltJetpackVersion")
73 |
74 | // Lifecycle
75 | val lifecycleVersion = "2.4.1"
76 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
77 | implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
78 | implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
79 |
80 | // Navigation
81 | val navVersion = "2.4.1"
82 | implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
83 | implementation("androidx.navigation:navigation-compose:2.4.0-beta02")
84 |
85 | // Coroutine
86 | val coroutineVersion = "1.6.1"
87 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
88 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")
89 |
90 | // Timber
91 | implementation("com.jakewharton.timber:timber:5.0.1")
92 |
93 | // Retrofit
94 | implementation("com.squareup.retrofit2:retrofit:2.9.0")
95 |
96 | testImplementation("junit:junit:4.13.2")
97 | testImplementation("com.google.truth:truth:1.1.3")
98 |
99 | androidTestImplementation("androidx.test.ext:junit:1.1.3")
100 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
101 | }
102 |
--------------------------------------------------------------------------------
/ui/view/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/ui/view/consumer-rules.pro
--------------------------------------------------------------------------------
/ui/view/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.kts.
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
--------------------------------------------------------------------------------
/ui/view/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/ui/view/src/main/java/jp/dosukoi/ui/view/common/ActivityNavigation.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.view.common
2 |
3 | import android.content.Context
4 | import android.widget.Toast
5 | import jp.dosukoi.ui.view.R
6 | import retrofit2.HttpException
7 | import java.io.IOException
8 |
9 | fun Context.showErrorToast(throwable: Throwable) {
10 | val message = when (throwable) {
11 | is IOException -> getString(R.string.io_exception)
12 | is HttpException -> when (throwable.code()) {
13 | 401, 403 -> getString(R.string.authorize_exception)
14 | else -> getString(R.string.general_exception)
15 | }
16 | else -> getString(R.string.general_exception)
17 | }
18 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
19 | }
20 |
--------------------------------------------------------------------------------
/ui/view/src/main/java/jp/dosukoi/ui/view/common/AppBar.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.view.common
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material.*
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.filled.ArrowBack
8 | import androidx.compose.material.icons.filled.Close
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.graphics.vector.ImageVector
12 |
13 | @Composable
14 | fun AppBarScaffold(
15 | title: String,
16 | content: @Composable () -> Unit,
17 | bottomNavigation: @Composable (() -> Unit)? = null,
18 | navigationIcon: ImageVector? = null,
19 | onNavigationClick: (() -> Unit)? = null
20 | ) {
21 | Scaffold(
22 | topBar = {
23 | TopAppBar(
24 | title = { Text(title) },
25 | navigationIcon = navigationIcon?.let {
26 | {
27 | IconButton(onClick = { onNavigationClick?.invoke() }) {
28 | Icon(imageVector = it, contentDescription = null)
29 | }
30 | }
31 | }
32 | )
33 | },
34 | bottomBar = { bottomNavigation?.invoke() },
35 | ) { innerPadding ->
36 | Box(Modifier.padding(innerPadding)) {
37 | content.invoke()
38 | }
39 | }
40 | }
41 |
42 | @Composable
43 | fun BackAppBarScaffold(
44 | title: String,
45 | content: @Composable () -> Unit,
46 | bottomNavigation: @Composable (() -> Unit)? = null,
47 | onNavigationClick: (() -> Unit)
48 | ) {
49 | AppBarScaffold(
50 | title = title,
51 | content = content,
52 | bottomNavigation = bottomNavigation,
53 | navigationIcon = Icons.Filled.ArrowBack,
54 | onNavigationClick = onNavigationClick
55 | )
56 | }
57 |
58 | @Composable
59 | fun CloseAppbarScaffold(
60 | title: String,
61 | content: @Composable () -> Unit,
62 | bottomNavigation: @Composable (() -> Unit)? = null,
63 | onNavigationClick: (() -> Unit)
64 | ) {
65 | AppBarScaffold(
66 | title = title,
67 | content = content,
68 | bottomNavigation = bottomNavigation,
69 | navigationIcon = Icons.Filled.Close,
70 | onNavigationClick = onNavigationClick
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/ui/view/src/main/java/jp/dosukoi/ui/view/common/AppColors.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.view.common
2 |
3 | import androidx.compose.material.Colors
4 | import androidx.compose.ui.graphics.Color
5 |
6 | fun appColors(): Colors {
7 | return Colors(
8 | primary = primaryColor,
9 | primaryVariant = primaryColor,
10 | secondary = black,
11 | secondaryVariant = black,
12 | background = primaryColor,
13 | surface = white,
14 | error = error,
15 | onPrimary = black,
16 | onSecondary = white,
17 | onBackground = black,
18 | onSurface = black,
19 | onError = error,
20 | isLight = true
21 | )
22 | }
23 |
24 | val primaryColor = Color.White
25 | val black = Color.Black
26 | val white = Color.White
27 | val gray = Color.Gray
28 | val whiteGray = Color(0xFFB8B8BC)
29 | val error = Color(0xFFFF6A56)
30 |
--------------------------------------------------------------------------------
/ui/view/src/main/java/jp/dosukoi/ui/view/common/CompositionLocalProvider.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.view.common
2 |
3 | import android.content.Context
4 | import coil.ImageLoader
5 | import okhttp3.OkHttpClient
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 | @Singleton
10 | class CompositionLocalProvider @Inject constructor(
11 | private val context: Context,
12 | private val okHttpClient: OkHttpClient
13 | ) {
14 |
15 | fun provideImageLoader() = ImageLoader.Builder(context).okHttpClient(okHttpClient).build()
16 | }
17 |
--------------------------------------------------------------------------------
/ui/view/src/main/java/jp/dosukoi/ui/view/common/LazyListStateEx.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.view.common
2 |
3 | import androidx.compose.foundation.lazy.LazyListState
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.derivedStateOf
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.snapshotFlow
10 | import kotlinx.coroutines.flow.filter
11 |
12 | @Composable
13 | fun OnScrollEnd(
14 | lazyListState: LazyListState,
15 | onAppearLastItem: () -> Unit
16 | ) {
17 | val isReachedLast by remember(lazyListState) {
18 | derivedStateOf {
19 | lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == lazyListState.layoutInfo.totalItemsCount - 1
20 | }
21 | }
22 |
23 | LaunchedEffect(lazyListState) {
24 | snapshotFlow { isReachedLast }
25 | .filter { it }
26 | .collect {
27 | onAppearLastItem()
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/ui/view/src/main/java/jp/dosukoi/ui/view/common/LoadingAndErrorScreen.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.view.common
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.size
10 | import androidx.compose.material.Button
11 | import androidx.compose.material.ButtonDefaults
12 | import androidx.compose.material.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.getValue
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.layout.ContentScale
19 | import androidx.compose.ui.text.TextStyle
20 | import androidx.compose.ui.text.font.FontWeight
21 | import androidx.compose.ui.text.style.TextAlign
22 | import androidx.compose.ui.tooling.preview.Preview
23 | import androidx.compose.ui.unit.dp
24 | import androidx.compose.ui.unit.sp
25 | import com.airbnb.lottie.compose.LottieAnimation
26 | import com.airbnb.lottie.compose.LottieCompositionSpec
27 | import com.airbnb.lottie.compose.rememberLottieComposition
28 | import jp.dosukoi.ui.view.R
29 | import jp.dosukoi.ui.viewmodel.common.LoadState
30 |
31 | @Composable
32 | fun LoadingAndErrorScreen(
33 | state: LoadState,
34 | loadedContent: @Composable (T) -> Unit,
35 | onRetryClick: () -> Unit
36 | ) {
37 | when (state) {
38 | LoadState.Loading -> LottieLoadingAnimation()
39 | is LoadState.Loaded -> loadedContent.invoke(state.data)
40 | is LoadState.Error -> LoadErrorContent(onRetryClick)
41 | }
42 | }
43 |
44 | @Composable
45 | fun LottieLoadingAnimation() {
46 | Box(
47 | modifier = Modifier
48 | .fillMaxSize()
49 | .background(Color.White)
50 | ) {
51 | val composition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.loading_animation))
52 | LottieAnimation(modifier = Modifier.align(Alignment.Center), composition = composition)
53 | }
54 | }
55 |
56 | @Composable
57 | private fun LoadErrorContent(
58 | onRetryClick: () -> Unit
59 | ) {
60 | val composition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.error_animation))
61 | Column(
62 | modifier = Modifier.fillMaxSize(),
63 | verticalArrangement = Arrangement.Center,
64 | horizontalAlignment = Alignment.CenterHorizontally
65 | ) {
66 | Box(modifier = Modifier.size(120.dp)) {
67 | LottieAnimation(
68 | composition = composition,
69 | contentScale = ContentScale.Fit,
70 | )
71 | }
72 | Text(
73 | "Error!",
74 | modifier = Modifier.padding(top = 24.dp),
75 | style = TextStyle(color = error, fontSize = 36.sp, fontWeight = FontWeight.Bold)
76 | )
77 | Text(
78 | "Opps, something went wrong\nPlease retry",
79 | textAlign = TextAlign.Center,
80 | modifier = Modifier.padding(top = 10.dp)
81 | )
82 | Button(
83 | onClick = onRetryClick,
84 | colors = ButtonDefaults.buttonColors(
85 | backgroundColor = error
86 | ),
87 | modifier = Modifier.padding(top = 32.dp)
88 | ) {
89 | Text(text = "Try Again", style = TextStyle(color = white))
90 | }
91 | }
92 | }
93 |
94 | @Preview
95 | @Composable
96 | private fun PreviewLoadErrorContent() {
97 | LoadErrorContent {
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/ui/view/src/main/java/jp/dosukoi/ui/view/common/ViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.view.common
2 |
3 | import androidx.activity.ComponentActivity
4 | import androidx.activity.viewModels
5 | import androidx.annotation.MainThread
6 | import androidx.lifecycle.AbstractSavedStateViewModelFactory
7 | import androidx.lifecycle.SavedStateHandle
8 | import androidx.lifecycle.ViewModel
9 |
10 | @MainThread
11 | inline fun ComponentActivity.produceViewModels(
12 | noinline factory: ((SavedStateHandle) -> VM)
13 | ): Lazy = viewModels { ActivitySaveStateViewModelProvider(this, factory) }
14 |
15 | class ActivitySaveStateViewModelProvider(
16 | activity: ComponentActivity,
17 | private val factory: (SavedStateHandle) -> VM
18 | ) : AbstractSavedStateViewModelFactory(activity, activity.intent?.extras) {
19 | @Suppress("UNCHECKED_CAST")
20 | override fun create(
21 | key: String,
22 | modelClass: Class,
23 | handle: SavedStateHandle
24 | ): T = factory(handle) as T
25 | }
26 |
--------------------------------------------------------------------------------
/ui/view/src/main/java/jp/dosukoi/ui/view/myPage/MyPageComponent.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.view.myPage
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import androidx.compose.foundation.Image
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.clickable
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.Column
10 | import androidx.compose.foundation.layout.PaddingValues
11 | import androidx.compose.foundation.layout.Row
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.foundation.layout.size
15 | import androidx.compose.foundation.lazy.LazyColumn
16 | import androidx.compose.foundation.lazy.itemsIndexed
17 | import androidx.compose.foundation.shape.CircleShape
18 | import androidx.compose.foundation.shape.RoundedCornerShape
19 | import androidx.compose.material.Card
20 | import androidx.compose.material.Divider
21 | import androidx.compose.material.Text
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.runtime.remember
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.draw.clip
27 | import androidx.compose.ui.graphics.Color
28 | import androidx.compose.ui.layout.ContentScale
29 | import androidx.compose.ui.platform.LocalContext
30 | import androidx.compose.ui.text.TextStyle
31 | import androidx.compose.ui.text.font.FontWeight
32 | import androidx.compose.ui.tooling.preview.Preview
33 | import androidx.compose.ui.unit.dp
34 | import androidx.compose.ui.unit.sp
35 | import coil.compose.rememberImagePainter
36 | import com.google.accompanist.swiperefresh.SwipeRefresh
37 | import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
38 | import jp.dosukoi.githubclient.domain.entity.myPage.Repository
39 | import jp.dosukoi.githubclient.domain.entity.myPage.User
40 | import jp.dosukoi.ui.view.common.gray
41 | import jp.dosukoi.ui.view.common.whiteGray
42 |
43 | @Composable
44 | fun MyPageComponent(
45 | user: User,
46 | repositoryList: List,
47 | isRefreshing: Boolean?,
48 | onRefresh: () -> Unit
49 | ) {
50 | SwipeRefresh(state = rememberSwipeRefreshState(isRefreshing ?: false), onRefresh = onRefresh) {
51 | LazyColumn(
52 | contentPadding = PaddingValues(vertical = 20.dp)
53 | ) {
54 | item { UserInfoCard(user) }
55 | itemsIndexed(repositoryList) { index, repository ->
56 | RepositoryItem(
57 | repository = repository,
58 | isLastItem = index == repositoryList.size
59 | )
60 | }
61 | }
62 | }
63 | }
64 |
65 | @Composable
66 | fun UserInfoCard(user: User) {
67 | val context = LocalContext.current
68 | val intent = remember {
69 | Intent(Intent.ACTION_VIEW, Uri.parse(user.htmlUrl))
70 | }
71 | Card(
72 | modifier = Modifier
73 | .padding(vertical = 10.dp, horizontal = 16.dp)
74 | .fillMaxWidth()
75 | .clickable { context.startActivity(intent) },
76 | shape = RoundedCornerShape(10.dp),
77 | elevation = 4.dp
78 | ) {
79 | Row(
80 | modifier = Modifier
81 | .fillMaxWidth()
82 | .padding(all = 16.dp),
83 | verticalAlignment = Alignment.CenterVertically,
84 | ) {
85 | UserIcon(user.avatarUrl)
86 | UserInfo(user)
87 | }
88 | }
89 | }
90 |
91 | @Composable
92 | fun UserIcon(imageUrl: String) {
93 | Box(
94 | modifier =
95 | Modifier
96 | .size(120.dp)
97 | .background(color = Color.White, shape = CircleShape)
98 | .clip(CircleShape)
99 |
100 | ) {
101 | Image(
102 | painter = rememberImagePainter(data = imageUrl),
103 | contentDescription = null,
104 | modifier = Modifier.fillMaxWidth(),
105 | contentScale = ContentScale.FillWidth
106 | )
107 | }
108 | }
109 |
110 | @Composable
111 | fun UserInfo(user: User) {
112 | Column(
113 | modifier = Modifier
114 | .padding(start = 24.dp, top = 16.dp, end = 16.dp, bottom = 16.dp)
115 | .fillMaxWidth()
116 | ) {
117 | Text(user.login, style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold))
118 | Text(
119 | user.company ?: "",
120 | modifier = Modifier.padding(top = 18.dp),
121 | style = TextStyle(fontSize = 14.sp)
122 | )
123 | Text(user.bio ?: "", Modifier.padding(top = 6.dp), style = TextStyle(fontSize = 14.sp))
124 | }
125 | }
126 |
127 | @Composable
128 | fun RepositoryItem(
129 | repository: Repository,
130 | isLastItem: Boolean
131 | ) {
132 | val context = LocalContext.current
133 | val intent = remember {
134 | Intent(Intent.ACTION_VIEW, Uri.parse(repository.htmlUrl))
135 | }
136 | Column(
137 | modifier = Modifier
138 | .fillMaxWidth()
139 | .clickable {
140 | context.startActivity(intent)
141 | }
142 | ) {
143 | Text(
144 | repository.fullName,
145 | style = TextStyle(fontSize = 18.sp),
146 | modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 10.dp)
147 | )
148 | Text(
149 | repository.description ?: "",
150 | style = TextStyle(fontSize = 14.sp, color = gray),
151 | modifier = Modifier.padding(top = 6.dp, bottom = 10.dp, start = 16.dp, end = 16.dp)
152 | )
153 | if (!isLastItem) Divider(color = whiteGray)
154 | }
155 | }
156 |
157 | //region Preview
158 | @Preview
159 | @Composable
160 | private fun UserInfoCardPreview() {
161 | UserInfoCard(
162 | user = User(
163 | "Dosukoi",
164 | 0L,
165 | "https://placehold.jp/150x150.png",
166 | "",
167 | "",
168 | null,
169 | null,
170 | null
171 | )
172 | )
173 | }
174 | //endregion
175 |
--------------------------------------------------------------------------------
/ui/view/src/main/java/jp/dosukoi/ui/view/myPage/MyPageScreen.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.view.myPage
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.ui.platform.LocalContext
7 | import jp.dosukoi.githubclient.domain.entity.myPage.UserStatus
8 | import jp.dosukoi.ui.view.common.LoadingAndErrorScreen
9 | import jp.dosukoi.ui.view.common.showErrorToast
10 | import jp.dosukoi.ui.viewmodel.myPage.MyPageViewModel
11 |
12 | @Composable
13 | fun MyPageScreen(
14 | viewModel: MyPageViewModel,
15 | ) {
16 | val myPageUiState by viewModel.myPageState.collectAsState()
17 | if (myPageUiState.errors.isNotEmpty()) {
18 | val context = LocalContext.current
19 | val throwable = myPageUiState.errors.first()
20 | context.showErrorToast(throwable)
21 | viewModel.onConsumeErrors(throwable)
22 | }
23 | LoadingAndErrorScreen(
24 | state = myPageUiState.screenState,
25 | loadedContent = { data ->
26 | when (val state = data.userStatus) {
27 | is UserStatus.Authenticated -> MyPageComponent(
28 | state.user,
29 | data.repositoryList,
30 | myPageUiState.isRefreshing,
31 | viewModel::onRefresh
32 | )
33 | UserStatus.UnAuthenticated -> UnAuthenticatedUserComponent()
34 | }
35 | },
36 | onRetryClick = viewModel::onRetryClick
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/ui/view/src/main/java/jp/dosukoi/ui/view/myPage/UnAuthenticatedUserComponent.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.view.myPage
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.material.Button
10 | import androidx.compose.material.ButtonDefaults
11 | import androidx.compose.material.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.ui.Alignment.Companion.CenterHorizontally
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.platform.LocalContext
17 | import androidx.compose.ui.unit.dp
18 | import jp.dosukoi.ui.view.common.white
19 |
20 | private const val CLIENT_ID = "52b65f6025ea1e4264cd"
21 | private const val VERIFY_URL =
22 | "https://github.com/login/oauth/authorize?client_id=$CLIENT_ID&scope=user repo"
23 |
24 | @Composable
25 | fun UnAuthenticatedUserComponent() {
26 | val context = LocalContext.current
27 | val intent = remember {
28 | Intent(Intent.ACTION_VIEW, Uri.parse(VERIFY_URL))
29 | }
30 | Column(
31 | modifier = Modifier.fillMaxSize(),
32 | horizontalAlignment = CenterHorizontally,
33 | verticalArrangement = Arrangement.Center
34 | ) {
35 | Button(
36 | onClick = { context.startActivity(intent) },
37 | Modifier.background(white),
38 | elevation = ButtonDefaults.elevation(defaultElevation = 8.dp)
39 | ) {
40 | Text("Login")
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/ui/view/src/main/java/jp/dosukoi/ui/view/search/SearchComponent.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.view.search
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.PaddingValues
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.lazy.LazyColumn
10 | import androidx.compose.foundation.lazy.LazyListState
11 | import androidx.compose.foundation.lazy.itemsIndexed
12 | import androidx.compose.foundation.text.KeyboardActions
13 | import androidx.compose.foundation.text.KeyboardOptions
14 | import androidx.compose.material.CircularProgressIndicator
15 | import androidx.compose.material.Icon
16 | import androidx.compose.material.IconButton
17 | import androidx.compose.material.Text
18 | import androidx.compose.material.TextField
19 | import androidx.compose.material.TextFieldDefaults
20 | import androidx.compose.material.icons.Icons
21 | import androidx.compose.material.icons.filled.Search
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.ui.Alignment
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.platform.LocalFocusManager
26 | import androidx.compose.ui.text.input.ImeAction
27 | import androidx.compose.ui.text.style.TextAlign
28 | import androidx.compose.ui.tooling.preview.Preview
29 | import androidx.compose.ui.unit.dp
30 | import jp.dosukoi.githubclient.domain.entity.myPage.Repository
31 | import jp.dosukoi.ui.view.common.LoadingAndErrorScreen
32 | import jp.dosukoi.ui.view.common.black
33 | import jp.dosukoi.ui.view.common.white
34 | import jp.dosukoi.ui.view.myPage.RepositoryItem
35 | import jp.dosukoi.ui.viewmodel.search.SearchState
36 | import jp.dosukoi.ui.viewmodel.search.SearchUiState
37 |
38 | @Composable
39 | fun SearchComponent(
40 | uiState: SearchUiState,
41 | listState: LazyListState,
42 | onValueChanged: (String) -> Unit,
43 | onSearchButtonClick: () -> Unit,
44 | onRetryClick: () -> Unit
45 | ) {
46 | Column(
47 | modifier = Modifier
48 | .fillMaxSize()
49 | ) {
50 | SearchTextField(
51 | searchText = uiState.searchWord,
52 | isTextError = uiState.isSearchWordError,
53 | onValueChanged = onValueChanged,
54 | onSearchButtonClick = onSearchButtonClick
55 | )
56 | SearchList(
57 | uiState = uiState,
58 | listState = listState,
59 | onRetryClick = onRetryClick
60 | )
61 | }
62 | }
63 |
64 | @Composable
65 | fun SearchTextField(
66 | searchText: String,
67 | isTextError: Boolean?,
68 | onValueChanged: (String) -> Unit,
69 | onSearchButtonClick: () -> Unit
70 | ) {
71 | val focusManager = LocalFocusManager.current
72 | val onSearch = {
73 | onSearchButtonClick.invoke()
74 | focusManager.clearFocus()
75 | }
76 | TextField(
77 | modifier = Modifier
78 | .fillMaxWidth()
79 | .padding(top = 10.dp, start = 16.dp, end = 16.dp),
80 | value = searchText,
81 | onValueChange = onValueChanged,
82 | colors = TextFieldDefaults.textFieldColors(
83 | textColor = black,
84 | backgroundColor = white,
85 | cursorColor = black.copy(alpha = 0.42f),
86 | trailingIconColor = black,
87 | focusedIndicatorColor = black.copy(alpha = 0.42f)
88 | ),
89 | trailingIcon = {
90 | IconButton(
91 | onClick = onSearch,
92 | content = {
93 | Icon(
94 | imageVector = Icons.Filled.Search,
95 | contentDescription = null
96 | )
97 | }
98 | )
99 | },
100 | placeholder = {
101 | Text(text = "Search")
102 | },
103 | isError = isTextError ?: false,
104 | maxLines = 1,
105 | keyboardActions = KeyboardActions(onSearch = { onSearch.invoke() }),
106 | keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Search)
107 | )
108 | }
109 |
110 | @Composable
111 | fun SearchList(
112 | uiState: SearchUiState,
113 | listState: LazyListState,
114 | onRetryClick: () -> Unit
115 | ) {
116 | LoadingAndErrorScreen(
117 | state = uiState.searchState,
118 | loadedContent = {
119 | when (it) {
120 | SearchState.Initialized -> SearchInitialComponent()
121 | SearchState.Empty -> SearchedEmptyComponent()
122 | is SearchState.Data ->
123 | SearchedListComponent(
124 | it.repositoryList,
125 | it.hasMore,
126 | listState
127 | )
128 | }
129 | },
130 | onRetryClick = onRetryClick
131 | )
132 | }
133 |
134 | @Composable
135 | fun SearchedListComponent(
136 | repositoryList: List,
137 | hasMore: Boolean,
138 | listState: LazyListState
139 | ) {
140 | LazyColumn(
141 | contentPadding = PaddingValues(top = 10.dp, bottom = 16.dp),
142 | state = listState
143 | ) {
144 | itemsIndexed(repositoryList) { index, repository ->
145 | RepositoryItem(
146 | repository = repository,
147 | isLastItem = index == repositoryList.size && !hasMore
148 | )
149 | }
150 | if (hasMore) item { LoadingFooter() }
151 | }
152 | }
153 |
154 | @Composable
155 | fun SearchInitialComponent() {
156 | Column(
157 | modifier = Modifier
158 | .fillMaxSize()
159 | .padding(start = 16.dp, end = 16.dp),
160 | verticalArrangement = Arrangement.Center,
161 | horizontalAlignment = Alignment.CenterHorizontally
162 | ) {
163 | Text(
164 | text = "Please enter a search word to search the repository",
165 | textAlign = TextAlign.Center
166 | )
167 | }
168 | }
169 |
170 | @Composable
171 | fun SearchedEmptyComponent() {
172 | Column(
173 | modifier = Modifier
174 | .fillMaxSize()
175 | .padding(start = 16.dp, end = 16.dp),
176 | verticalArrangement = Arrangement.Center,
177 | horizontalAlignment = Alignment.CenterHorizontally
178 | ) {
179 | Text(
180 | text = "Oops, Repository is not found.\n" +
181 | "Please change search word and retry again.",
182 | textAlign = TextAlign.Center
183 | )
184 | }
185 | }
186 |
187 | @Composable
188 | fun LoadingFooter() {
189 | Column(
190 | modifier = Modifier.fillMaxWidth(),
191 | verticalArrangement = Arrangement.Center,
192 | horizontalAlignment = Alignment.CenterHorizontally
193 | ) {
194 | CircularProgressIndicator(
195 | modifier = Modifier.padding(10.dp),
196 | color = black
197 | )
198 | }
199 | }
200 |
201 | @Preview
202 | @Composable
203 | fun PreviewSearchTextField() {
204 | SearchTextField(
205 | searchText = "hoge",
206 | isTextError = true,
207 | onValueChanged = {},
208 | onSearchButtonClick = {})
209 | }
210 |
211 | @Preview
212 | @Composable
213 | fun PreviewSearchedEmptyComponent() {
214 | SearchedEmptyComponent()
215 | }
216 |
--------------------------------------------------------------------------------
/ui/view/src/main/java/jp/dosukoi/ui/view/search/SearchPageScreen.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.view.search
2 |
3 | import androidx.compose.foundation.lazy.rememberLazyListState
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.collectAsState
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.ui.platform.LocalContext
8 | import jp.dosukoi.ui.view.common.OnScrollEnd
9 | import jp.dosukoi.ui.view.common.showErrorToast
10 | import jp.dosukoi.ui.viewmodel.search.SearchViewModel
11 |
12 | @Composable
13 | fun SearchScreen(
14 | viewModel: SearchViewModel
15 | ) {
16 | val uiState by viewModel.searchUiState.collectAsState()
17 | if (uiState.errors.isNotEmpty()) {
18 | val context = LocalContext.current
19 | val throwable = uiState.errors.first()
20 | context.showErrorToast(throwable)
21 | viewModel.onConsumeErrors(throwable)
22 | }
23 | val listState = rememberLazyListState()
24 | OnScrollEnd(lazyListState = listState, onAppearLastItem = viewModel::onScrollEnd)
25 | SearchComponent(
26 | uiState,
27 | listState,
28 | viewModel::onSearchWordChanged,
29 | viewModel::onSearchButtonClick,
30 | viewModel::onRetryClick
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/ui/view/src/main/java/jp/dosukoi/ui/view/top/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.view.top
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.viewModels
7 | import androidx.appcompat.app.AppCompatActivity
8 | import androidx.compose.material.MaterialTheme
9 | import androidx.compose.runtime.CompositionLocalProvider
10 | import coil.compose.LocalImageLoader
11 | import dagger.hilt.android.AndroidEntryPoint
12 | import jp.dosukoi.ui.view.common.CompositionLocalProvider
13 | import jp.dosukoi.ui.view.common.appColors
14 | import jp.dosukoi.ui.viewmodel.myPage.MyPageViewModel
15 | import jp.dosukoi.ui.viewmodel.search.SearchViewModel
16 | import javax.inject.Inject
17 |
18 | @AndroidEntryPoint
19 | class MainActivity : AppCompatActivity() {
20 |
21 | private val myPageViewModel: MyPageViewModel by viewModels()
22 |
23 | private val searchViewModel: SearchViewModel by viewModels()
24 |
25 | @Inject
26 | lateinit var compositionLocalProvider: CompositionLocalProvider
27 |
28 | override fun onCreate(savedInstanceState: Bundle?) {
29 | super.onCreate(savedInstanceState)
30 |
31 | setContent {
32 | MaterialTheme(
33 | colors = appColors()
34 | ) {
35 | CompositionLocalProvider(
36 | LocalImageLoader provides compositionLocalProvider.provideImageLoader()
37 | ) {
38 | TopScreen(
39 | searchViewModel,
40 | myPageViewModel,
41 | )
42 | }
43 | }
44 | }
45 |
46 | myPageViewModel.init()
47 | }
48 |
49 | override fun onNewIntent(intent: Intent?) {
50 | super.onNewIntent(intent)
51 | val action = intent?.action
52 | if (action == null || action != Intent.ACTION_VIEW) {
53 | return
54 | }
55 | val uri = intent.data ?: return
56 | val code = uri.getQueryParameter("code")
57 | myPageViewModel.onGetCode(code)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/ui/view/src/main/java/jp/dosukoi/ui/view/top/TopScreen.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.view.top
2 |
3 | import androidx.compose.material.BottomNavigation
4 | import androidx.compose.material.BottomNavigationItem
5 | import androidx.compose.material.Icon
6 | import androidx.compose.material.Text
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.filled.Person
9 | import androidx.compose.material.icons.filled.Search
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.ui.graphics.vector.ImageVector
13 | import androidx.navigation.compose.NavHost
14 | import androidx.navigation.compose.composable
15 | import androidx.navigation.compose.currentBackStackEntryAsState
16 | import androidx.navigation.compose.rememberNavController
17 | import jp.dosukoi.ui.view.common.AppBarScaffold
18 | import jp.dosukoi.ui.view.common.black
19 | import jp.dosukoi.ui.view.common.gray
20 | import jp.dosukoi.ui.view.common.white
21 | import jp.dosukoi.ui.view.myPage.MyPageScreen
22 | import jp.dosukoi.ui.view.search.SearchScreen
23 | import jp.dosukoi.ui.viewmodel.myPage.MyPageViewModel
24 | import jp.dosukoi.ui.viewmodel.search.SearchViewModel
25 |
26 | @Composable
27 | fun TopScreen(
28 | searchViewModel: SearchViewModel,
29 | myPageViewModel: MyPageViewModel
30 | ) {
31 | val bottomNavigationItemList = listOf(
32 | TopScreens.Search,
33 | TopScreens.MyPage
34 | )
35 | val navController = rememberNavController()
36 | val navBackStackEntry by navController.currentBackStackEntryAsState()
37 | val currentRoute = navBackStackEntry?.destination?.route
38 | AppBarScaffold(title = "GitHubClient", bottomNavigation = {
39 | BottomNavigation(
40 | backgroundColor = white
41 | ) {
42 | bottomNavigationItemList.forEach { item ->
43 | BottomNavigationItem(
44 | selected = item.route == currentRoute,
45 | onClick = { navController.navigate(item.route) },
46 | icon = { Icon(imageVector = item.icon, contentDescription = null) },
47 | label = { Text(text = item.title) },
48 | selectedContentColor = black,
49 | unselectedContentColor = gray.copy(alpha = 0.4f)
50 | )
51 | }
52 | }
53 | }, content = {
54 | NavHost(navController = navController, startDestination = TopScreens.MyPage.route) {
55 | composable(TopScreens.Search.route) {
56 | SearchScreen(searchViewModel)
57 | }
58 | composable(TopScreens.MyPage.route) {
59 | MyPageScreen(
60 | myPageViewModel,
61 | )
62 | }
63 | }
64 | })
65 | }
66 |
67 | sealed class TopScreens(val route: String, val title: String, val icon: ImageVector) {
68 | object Search : TopScreens("search", "Search", Icons.Filled.Search)
69 | object MyPage : TopScreens("myPage", "MyPage", Icons.Filled.Person)
70 | }
71 |
--------------------------------------------------------------------------------
/ui/view/src/main/java/jp/dosukoi/ui/view/widget/GlanceAppWidgetSample.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.view.widget
2 |
3 | import android.content.Context
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.graphics.Color
6 | import androidx.compose.ui.unit.dp
7 | import androidx.datastore.preferences.core.Preferences
8 | import androidx.datastore.preferences.core.stringPreferencesKey
9 | import androidx.glance.GlanceId
10 | import androidx.glance.GlanceModifier
11 | import androidx.glance.action.ActionParameters
12 | import androidx.glance.action.clickable
13 | import androidx.glance.appwidget.GlanceAppWidget
14 | import androidx.glance.appwidget.GlanceAppWidgetReceiver
15 | import androidx.glance.appwidget.action.ActionCallback
16 | import androidx.glance.appwidget.action.actionRunCallback
17 | import androidx.glance.appwidget.background
18 | import androidx.glance.appwidget.cornerRadius
19 | import androidx.glance.appwidget.state.updateAppWidgetState
20 | import androidx.glance.currentState
21 | import androidx.glance.layout.Box
22 | import androidx.glance.layout.fillMaxSize
23 | import androidx.glance.layout.padding
24 | import androidx.glance.state.GlanceStateDefinition
25 | import androidx.glance.state.PreferencesGlanceStateDefinition
26 | import androidx.glance.text.Text
27 | import dagger.hilt.android.AndroidEntryPoint
28 | import java.util.UUID
29 | import javax.inject.Inject
30 |
31 | class GlanceAppWidgetSample @Inject constructor() : GlanceAppWidget() {
32 |
33 | companion object {
34 | val state = stringPreferencesKey("state")
35 | }
36 |
37 | override val stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition
38 |
39 | @Composable
40 | override fun Content() {
41 | val prefs = currentState()
42 | Box(
43 | modifier = GlanceModifier
44 | .fillMaxSize()
45 | .background(day = Color.White, night = Color.Black)
46 | .padding(8.dp)
47 | .cornerRadius(10.dp)
48 | .clickable(actionRunCallback())
49 | ) {
50 | Text(text = prefs[state] ?: "")
51 | }
52 | }
53 | }
54 |
55 | class GlanceAppWidgetSampleAction @Inject constructor() : ActionCallback {
56 | override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
57 | GlanceAppWidgetSample().apply {
58 | updateAppWidgetState(context, glanceId) { prefs ->
59 | prefs.toMutablePreferences().apply {
60 | set(GlanceAppWidgetSample.state, UUID.randomUUID().toString())
61 | }
62 | }
63 | update(context, glanceId)
64 | }
65 | }
66 | }
67 |
68 | @AndroidEntryPoint
69 | class GlanceAppWidgetProviderSample : GlanceAppWidgetReceiver() {
70 |
71 | @Inject
72 | lateinit var glanceAppWidgetSample: GlanceAppWidgetSample
73 |
74 | override val glanceAppWidget: GlanceAppWidget
75 | get() = glanceAppWidgetSample
76 | }
77 |
--------------------------------------------------------------------------------
/ui/view/src/main/res/drawable-anydpi/ic_arrow_back.xml:
--------------------------------------------------------------------------------
1 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/ui/view/src/main/res/drawable-anydpi/ic_my_page.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/ui/view/src/main/res/drawable-anydpi/ic_search.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/ui/view/src/main/res/drawable-hdpi/ic_arrow_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/ui/view/src/main/res/drawable-hdpi/ic_arrow_back.png
--------------------------------------------------------------------------------
/ui/view/src/main/res/drawable-hdpi/ic_my_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/ui/view/src/main/res/drawable-hdpi/ic_my_page.png
--------------------------------------------------------------------------------
/ui/view/src/main/res/drawable-hdpi/ic_search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/ui/view/src/main/res/drawable-hdpi/ic_search.png
--------------------------------------------------------------------------------
/ui/view/src/main/res/drawable-mdpi/ic_arrow_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/ui/view/src/main/res/drawable-mdpi/ic_arrow_back.png
--------------------------------------------------------------------------------
/ui/view/src/main/res/drawable-mdpi/ic_my_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/ui/view/src/main/res/drawable-mdpi/ic_my_page.png
--------------------------------------------------------------------------------
/ui/view/src/main/res/drawable-mdpi/ic_search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/ui/view/src/main/res/drawable-mdpi/ic_search.png
--------------------------------------------------------------------------------
/ui/view/src/main/res/drawable-xhdpi/ic_arrow_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/ui/view/src/main/res/drawable-xhdpi/ic_arrow_back.png
--------------------------------------------------------------------------------
/ui/view/src/main/res/drawable-xhdpi/ic_my_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/ui/view/src/main/res/drawable-xhdpi/ic_my_page.png
--------------------------------------------------------------------------------
/ui/view/src/main/res/drawable-xhdpi/ic_search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/ui/view/src/main/res/drawable-xhdpi/ic_search.png
--------------------------------------------------------------------------------
/ui/view/src/main/res/drawable-xxhdpi/ic_arrow_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/ui/view/src/main/res/drawable-xxhdpi/ic_arrow_back.png
--------------------------------------------------------------------------------
/ui/view/src/main/res/drawable-xxhdpi/ic_my_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/ui/view/src/main/res/drawable-xxhdpi/ic_my_page.png
--------------------------------------------------------------------------------
/ui/view/src/main/res/drawable-xxhdpi/ic_search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/ui/view/src/main/res/drawable-xxhdpi/ic_search.png
--------------------------------------------------------------------------------
/ui/view/src/main/res/layout/glance_initial_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/ui/view/src/main/res/raw/error_animation.json:
--------------------------------------------------------------------------------
1 | {"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":30,"ip":0,"op":80,"w":500,"h":500,"nm":"exclamação animation","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"exclamation","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":7,"s":[258.4,231.36,0],"to":[0,-1.06,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[258.4,225,0],"to":[0,0,0],"ti":[0,-1.06,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":13,"s":[258.4,231.36,0],"to":[0,1.06,0],"ti":[0,1.06,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":41,"s":[258.4,231.36,0],"to":[0,-1.06,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":44,"s":[258.4,225,0],"to":[0,0,0],"ti":[0,-1.06,0]},{"t":47,"s":[258.4,231.36,0]}],"ix":2},"a":{"a":0,"k":[16.613,83.692,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":7,"s":[90,90,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[93,93,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":13,"s":[90,90,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":41,"s":[90,90,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":44,"s":[93,93,100]},{"t":47,"s":[90,90,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[9.037,0],[0,0],[0,9.038],[-9.037,0],[0,-9.037]],"o":[[0,0],[-9.037,0],[0,-9.037],[9.037,0],[0,9.038]],"v":[[0,16.363],[0,16.363],[-16.363,0],[0,-16.363],[16.363,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.613,150.771],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.134,0],[0,0],[0,8.134],[0,0],[-8.133,0],[0,-8.134],[0,0]],"o":[[0,0],[-8.133,0],[0,0],[0,-8.134],[8.134,0],[0,0],[0,8.134]],"v":[[0,59.999],[0,59.999],[-14.727,45.271],[-14.727,-45.271],[0,-59.999],[14.727,-45.271],[14.727,45.271]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.613,60.249],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":80,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"stroke circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[258.4,231.36,0],"ix":2},"a":{"a":0,"k":[146,146,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-77.872,0],[0,-77.872],[77.872,0],[0,77.872]],"o":[[77.872,0],[0,77.872],[-77.872,0],[0,-77.872]],"v":[[0,-141],[141,0],[0,141],[-141,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.952999997606,0.340999977261,0.301999978458,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[146,146],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":30,"s":[90,90]},{"t":78,"s":[120,120]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[90]},{"t":79,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"stroke circle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[258.4,231.36,0],"ix":2},"a":{"a":0,"k":[146,146,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-77.872,0],[0,-77.872],[77.872,0],[0,77.872]],"o":[[77.872,0],[0,77.872],[-77.872,0],[0,-77.872]],"v":[[0,-141],[141,0],[0,141],[-141,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.952999997606,0.340999977261,0.301999978458,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[146,146],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":0,"s":[90,90]},{"t":48,"s":[120,120]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[90]},{"t":49,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[258.4,231.36,0],"ix":2},"a":{"a":0,"k":[130.82,130.82,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-72.112,0],[0,-72.112],[72.111,0],[0,72.111]],"o":[[72.111,0],[0,72.111],[-72.112,0],[0,-72.112]],"v":[[0,-130.57],[130.57,0],[0,130.57],[-130.57,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.952999997606,0.340999977261,0.301999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[130.82,130.82],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"bm":0}],"markers":[]}
--------------------------------------------------------------------------------
/ui/view/src/main/res/raw/loading_animation.json:
--------------------------------------------------------------------------------
1 | {"assets":[],"layers":[{"ddd":0,"ind":0,"ty":4,"nm":"形状图层 5","ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":8,"s":[100],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":24,"s":[30],"e":[100]},{"t":40}]},"r":{"k":0},"p":{"k":[187.875,77.125,0]},"a":{"k":[-76.375,-2.875,0]},"s":{"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":8,"s":[100,100,100],"e":[200,200,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":24,"s":[200,200,100],"e":[100,100,100]},{"t":40}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"k":[18,18]},"p":{"k":[0,0]},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"k":[1,1,1,1]},"o":{"k":100},"w":{"k":0},"lc":1,"lj":1,"ml":4,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"k":[0.87,0.42,0.56,1]},"o":{"k":100},"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"k":[-76.482,-3.482],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":40,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":1,"ty":4,"nm":"形状图层 4","ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":6,"s":[100],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":22,"s":[30],"e":[100]},{"t":36}]},"r":{"k":0},"p":{"k":[162.125,76.625,0]},"a":{"k":[-76.375,-2.875,0]},"s":{"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":6,"s":[100,100,100],"e":[200,200,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":22,"s":[200,200,100],"e":[100,100,100]},{"t":36}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"k":[18,18]},"p":{"k":[0,0]},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"k":[1,1,1,1]},"o":{"k":100},"w":{"k":0},"lc":1,"lj":1,"ml":4,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"k":[0.81,0.55,0.82,1]},"o":{"k":100},"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"k":[-76.482,-3.482],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":40,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"形状图层 3","ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":4,"s":[100],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":20,"s":[30],"e":[100]},{"t":32}]},"r":{"k":0},"p":{"k":[135.625,76.625,0]},"a":{"k":[-76.375,-2.875,0]},"s":{"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":4,"s":[100,100,100],"e":[200,200,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":20,"s":[200,200,100],"e":[100,100,100]},{"t":32}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"k":[18,18]},"p":{"k":[0,0]},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"k":[1,1,1,1]},"o":{"k":100},"w":{"k":0},"lc":1,"lj":1,"ml":4,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"k":[0.47,0.31,0.62,1]},"o":{"k":100},"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"k":[-76.482,-3.482],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":40,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"形状图层 2","ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":2,"s":[100],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":16,"s":[30],"e":[100]},{"t":28}]},"r":{"k":0},"p":{"k":[109.375,76.625,0]},"a":{"k":[-76.625,-3.125,0]},"s":{"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":2,"s":[100,100,100],"e":[200,200,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":16,"s":[200,200,100],"e":[100,100,100]},{"t":28}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"k":[18,18]},"p":{"k":[0,0]},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"k":[1,1,1,1]},"o":{"k":100},"w":{"k":0},"lc":1,"lj":1,"ml":4,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"k":[0.54,0.81,0.89,1]},"o":{"k":100},"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"k":[-76.482,-3.482],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":40,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":4,"ty":4,"nm":"形状图层 1","ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":0,"s":[100],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":12,"s":[30],"e":[100]},{"t":24}]},"r":{"k":0},"p":{"k":[82.625,76.625,0]},"a":{"k":[-76.625,-3.375,0]},"s":{"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":0,"s":[100,100,100],"e":[200,200,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":12,"s":[200,200,100],"e":[100,100,100]},{"t":24}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"k":[18,18]},"p":{"k":[0,0]},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"k":[1,1,1,1]},"o":{"k":100},"w":{"k":0},"lc":1,"lj":1,"ml":4,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"k":[0.34,0.45,0.78,1]},"o":{"k":100},"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"k":[-76.482,-3.482],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":40,"st":0,"bm":0,"sr":1}],"v":"4.5.4","ddd":0,"ip":0,"op":40,"fr":24,"w":280,"h":160}
--------------------------------------------------------------------------------
/ui/view/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FF000000
4 | #FFFFFFFF
5 |
--------------------------------------------------------------------------------
/ui/view/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 通信に失敗しました
4 | 認証に失敗しました
5 | エラーが発生しました
6 |
--------------------------------------------------------------------------------
/ui/view/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
12 |
--------------------------------------------------------------------------------
/ui/view/src/main/res/xml-v31/glance_app_widget_sample_meta_data.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------
/ui/view/src/main/res/xml/glance_app_widget_sample_meta_data.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------
/ui/viewModel/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/ui/viewModel/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | kotlin("kapt")
5 | id("dagger.hilt.android.plugin")
6 | id("kotlin-parcelize")
7 | }
8 |
9 | android {
10 | applyCommon()
11 |
12 | buildFeatures {
13 | dataBinding = true
14 | }
15 | }
16 |
17 | dependencies {
18 |
19 | implementation(project(":domain:entity"))
20 | implementation(project(":domain:usecase"))
21 | testImplementation(project(":testing"))
22 |
23 | // Hilt
24 | val hiltVersion = "2.41"
25 | val hiltJetpackVersion = "1.0.0"
26 | implementation("com.google.dagger:hilt-android:$hiltVersion")
27 |
28 | kapt("com.google.dagger:hilt-android-compiler:$hiltVersion")
29 | kapt("androidx.hilt:hilt-compiler:$hiltJetpackVersion")
30 |
31 | // Lifecycle
32 | val lifecycleVersion = "2.4.1"
33 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
34 | implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
35 |
36 | // Coroutine
37 | val coroutineVersion = "1.6.1"
38 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
39 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")
40 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutineVersion")
41 |
42 | // Timber
43 | implementation("com.jakewharton.timber:timber:5.0.1")
44 |
45 | // Serialization
46 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
47 |
48 | // Test
49 | val mockkVersion = "1.12.3"
50 | implementation("io.mockk:mockk:$mockkVersion")
51 | testImplementation("androidx.test.ext:junit-ktx:1.1.3")
52 |
53 |
54 | testImplementation("junit:junit:4.13.2")
55 | testImplementation("com.google.truth:truth:1.1.3")
56 | testImplementation("androidx.arch.core:core-testing:2.1.0")
57 | testImplementation("androidx.test:core-ktx:1.4.0")
58 | testImplementation("androidx.test:rules:1.4.0")
59 | testImplementation("androidx.test:runner:1.4.0")
60 | }
61 |
--------------------------------------------------------------------------------
/ui/viewModel/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dosukoi-android/GitHubClientForJetpackCompose/e2f7074d9b01c673c5faec3ab5a67128fd127489/ui/viewModel/consumer-rules.pro
--------------------------------------------------------------------------------
/ui/viewModel/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.kts.
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
--------------------------------------------------------------------------------
/ui/viewModel/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/ui/viewModel/src/main/java/jp/dosukoi/ui/viewmodel/common/LoadState.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.viewmodel.common
2 |
3 | sealed class LoadState {
4 | object Loading : LoadState()
5 | data class Loaded(val data: T) : LoadState()
6 | data class Error(val message: String) : LoadState()
7 | }
8 |
--------------------------------------------------------------------------------
/ui/viewModel/src/main/java/jp/dosukoi/ui/viewmodel/common/NoCacheMutableLiveData.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.viewmodel.common
2 |
3 | import androidx.lifecycle.LifecycleOwner
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.Observer
6 | import java.util.concurrent.atomic.AtomicBoolean
7 |
8 | /**
9 | * LiveData without cache.
10 | * Support screen regeneration unlike Transformations#distinctUntilChanged.
11 | */
12 | class NoCacheMutableLiveData : MutableLiveData() {
13 | private var shouldNotify = AtomicBoolean(false)
14 | private var observer: Observer? = null
15 |
16 | override fun observe(owner: LifecycleOwner, observer: Observer) {
17 | this.observer = observer
18 | super.observe(owner, ::considerNotify)
19 | }
20 |
21 | override fun observeForever(observer: Observer) {
22 | this.observer = observer
23 | super.observeForever(::considerNotify)
24 | }
25 |
26 | override fun setValue(value: T) {
27 | shouldNotify.set(true)
28 | super.setValue(value)
29 | }
30 |
31 | override fun postValue(value: T) {
32 | shouldNotify.set(true)
33 | super.postValue(value)
34 | }
35 |
36 | override fun removeObserver(observer: Observer) {
37 | super.removeObserver(observer)
38 | this.observer = null
39 | }
40 |
41 | private fun considerNotify(t: T) {
42 | if (shouldNotify.compareAndSet(true, false)) {
43 | observer?.onChanged(t)
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/ui/viewModel/src/main/java/jp/dosukoi/ui/viewmodel/common/WebViewExt.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.viewmodel.common
2 |
3 | import android.graphics.Bitmap
4 | import android.webkit.WebView
5 | import android.webkit.WebViewClient
6 | import androidx.databinding.BindingAdapter
7 |
8 | @BindingAdapter("url")
9 | fun WebView.setUrl(url: String?) {
10 | url ?: return
11 | this.loadUrl(url)
12 | }
13 |
14 | @BindingAdapter("postUrl", "postData")
15 | fun WebView.setPostUrl(url: String?, postData: ByteArray?) {
16 | url ?: return
17 | postData ?: return
18 | this.postUrl(url, postData)
19 | }
20 |
21 | @BindingAdapter("callback")
22 | fun WebView.setCallback(callback: WebViewCallback?) {
23 | callback ?: return
24 | this.webViewClient = object : WebViewClient() {
25 | override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
26 | callback.onPageStarted()
27 | }
28 |
29 | override fun onPageFinished(view: WebView?, url: String?) {
30 | callback.onPageFinished()
31 | }
32 | }
33 | }
34 |
35 | interface WebViewCallback {
36 | fun onPageStarted()
37 | fun onPageFinished()
38 | }
39 |
--------------------------------------------------------------------------------
/ui/viewModel/src/main/java/jp/dosukoi/ui/viewmodel/myPage/MyPageUiState.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.viewmodel.myPage
2 |
3 | import jp.dosukoi.githubclient.domain.entity.myPage.Repository
4 | import jp.dosukoi.githubclient.domain.entity.myPage.UserStatus
5 | import jp.dosukoi.ui.viewmodel.common.LoadState
6 |
7 | data class MyPageUiState(
8 | val screenState: LoadState = LoadState.Loading,
9 | val isRefreshing: Boolean = false,
10 | val errors: List = emptyList()
11 | )
12 |
13 | data class MyPageScreenState(
14 | val userStatus: UserStatus,
15 | val repositoryList: List
16 | )
17 |
--------------------------------------------------------------------------------
/ui/viewModel/src/main/java/jp/dosukoi/ui/viewmodel/myPage/MyPageViewModel.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.viewmodel.myPage
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import dagger.hilt.android.lifecycle.HiltViewModel
6 | import jp.dosukoi.githubclient.domain.entity.auth.UnAuthorizeException
7 | import jp.dosukoi.githubclient.domain.entity.myPage.UserStatus
8 | import jp.dosukoi.githubclient.domain.usecase.auth.GetAccessTokenUseCase
9 | import jp.dosukoi.githubclient.domain.usecase.myPage.GetRepositoriesUseCase
10 | import jp.dosukoi.githubclient.domain.usecase.myPage.GetUserStatusUseCase
11 | import jp.dosukoi.ui.viewmodel.common.LoadState
12 | import kotlinx.coroutines.flow.MutableStateFlow
13 | import kotlinx.coroutines.flow.StateFlow
14 | import kotlinx.coroutines.flow.update
15 | import kotlinx.coroutines.launch
16 | import timber.log.Timber
17 | import javax.inject.Inject
18 |
19 | @HiltViewModel
20 | class MyPageViewModel @Inject constructor(
21 | private val getUserStatusUseCase: GetUserStatusUseCase,
22 | private val getRepositoriesUseCase: GetRepositoriesUseCase,
23 | private val getAccessTokenUseCase: GetAccessTokenUseCase,
24 | ) : ViewModel() {
25 |
26 | private val _myPageState: MutableStateFlow =
27 | MutableStateFlow(MyPageUiState())
28 | val myPageState: StateFlow = _myPageState
29 |
30 | fun init() {
31 | refresh()
32 | }
33 |
34 | private fun refresh() {
35 | viewModelScope.launch {
36 | runCatching {
37 | MyPageScreenState(
38 | getUserStatusUseCase.execute(),
39 | getRepositoriesUseCase.execute(),
40 | )
41 | }.onSuccess { screenState ->
42 | _myPageState.update {
43 | it.copy(screenState = LoadState.Loaded(screenState), isRefreshing = false)
44 | }
45 | }.onFailure {
46 | Timber.w(it)
47 | when (it) {
48 | is UnAuthorizeException -> {
49 | _myPageState.update {
50 | it.copy(
51 | screenState = LoadState.Loaded(
52 | MyPageScreenState(
53 | UserStatus.UnAuthenticated,
54 | emptyList()
55 | )
56 | ),
57 | isRefreshing = false
58 | )
59 | }
60 | }
61 | else -> {
62 | _myPageState.update {
63 | it.copy(screenState = LoadState.Error("Error"))
64 | }
65 | }
66 | }
67 | }
68 | }
69 | }
70 |
71 | fun onRetryClick() {
72 | _myPageState.update {
73 | it.copy(screenState = LoadState.Loading)
74 | }
75 | refresh()
76 | }
77 |
78 | fun onRefresh() {
79 | _myPageState.update {
80 | when (it.screenState) {
81 | is LoadState.Loaded -> it.copy(isRefreshing = true)
82 | else -> it
83 | }
84 | }
85 | refresh()
86 | }
87 |
88 | fun onGetCode(code: String?) {
89 | code ?: return
90 | viewModelScope.launch {
91 | runCatching {
92 | getAccessTokenUseCase.execute(code)
93 | }.onSuccess {
94 | _myPageState.update {
95 | it.copy(screenState = LoadState.Loading)
96 | }
97 | refresh()
98 | }.onFailure { throwable ->
99 | _myPageState.update {
100 | it.copy(errors = it.errors.plus(throwable))
101 | }
102 | }
103 | }
104 | }
105 |
106 | fun onConsumeErrors(throwable: Throwable) {
107 | _myPageState.update {
108 | it.copy(errors = it.errors.minus(throwable))
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/ui/viewModel/src/main/java/jp/dosukoi/ui/viewmodel/search/SearchUiState.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.viewmodel.search
2 |
3 | import jp.dosukoi.githubclient.domain.entity.myPage.Repository
4 | import jp.dosukoi.ui.viewmodel.common.LoadState
5 |
6 | data class SearchUiState(
7 | val searchWord: String = "",
8 | val searchState: LoadState = LoadState.Loaded(SearchState.Initialized),
9 | val isSearchWordError: Boolean = false,
10 | val errors: List = emptyList()
11 | )
12 |
13 | sealed class SearchState {
14 | object Initialized : SearchState()
15 | data class Data(val repositoryList: List, val hasMore: Boolean) : SearchState()
16 | object Empty : SearchState()
17 | }
18 |
--------------------------------------------------------------------------------
/ui/viewModel/src/main/java/jp/dosukoi/ui/viewmodel/search/SearchViewModel.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.viewmodel.search
2 |
3 | import androidx.annotation.VisibleForTesting
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import dagger.hilt.android.lifecycle.HiltViewModel
7 | import jp.dosukoi.githubclient.domain.entity.search.Search
8 | import jp.dosukoi.githubclient.domain.usecase.search.GetSearchDataUseCase
9 | import jp.dosukoi.ui.viewmodel.common.LoadState
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.StateFlow
12 | import kotlinx.coroutines.flow.update
13 | import kotlinx.coroutines.launch
14 | import java.util.concurrent.atomic.AtomicBoolean
15 | import java.util.concurrent.atomic.AtomicInteger
16 | import javax.inject.Inject
17 |
18 | @HiltViewModel
19 | class SearchViewModel @Inject constructor(
20 | private val getSearchDataUseCase: GetSearchDataUseCase
21 | ) : ViewModel() {
22 |
23 | private val _searchUiState: MutableStateFlow =
24 | MutableStateFlow(SearchUiState())
25 | val searchUiState: StateFlow = _searchUiState
26 |
27 | @VisibleForTesting
28 | val isLoadingMore = AtomicBoolean()
29 | private val pageCount = AtomicInteger(1)
30 |
31 | fun onSearchWordChanged(text: String) {
32 | _searchUiState.update {
33 | it.copy(searchWord = text)
34 | }
35 | }
36 |
37 | fun onSearchButtonClick() {
38 | validateAndRefresh()
39 | }
40 |
41 | fun onRetryClick() {
42 | validateAndRefresh()
43 | }
44 |
45 | fun onScrollEnd() {
46 | when (val loadState = searchUiState.value.searchState) {
47 | is LoadState.Loaded -> {
48 | when (val searchState = loadState.data) {
49 | is SearchState.Data -> {
50 | if (searchState.hasMore && isLoadingMore.compareAndSet(false, true)) {
51 | refresh(false)
52 | }
53 | }
54 | else -> {
55 | // do nothing
56 | }
57 | }
58 | }
59 | else -> {
60 | // do nothing
61 | }
62 | }
63 | }
64 |
65 | fun onConsumeErrors(throwable: Throwable) {
66 | _searchUiState.update {
67 | it.copy(errors = it.errors.minus(throwable))
68 | }
69 | }
70 |
71 | private fun validateAndRefresh() {
72 | if (searchUiState.value.searchWord.isBlank()) {
73 | _searchUiState.update {
74 | it.copy(isSearchWordError = true)
75 | }
76 | return
77 | } else {
78 | pageCount.set(1)
79 | _searchUiState.update {
80 | it.copy(isSearchWordError = false)
81 | }
82 | }
83 | _searchUiState.update {
84 | when (val loadState = it.searchState) {
85 | is LoadState.Loaded -> when (loadState.data) {
86 | SearchState.Initialized, SearchState.Empty -> it.copy(searchState = LoadState.Loading)
87 | else -> it
88 | }
89 | else -> it.copy(searchState = LoadState.Loading)
90 | }
91 | }
92 | refresh(true)
93 | }
94 |
95 | private fun refresh(isRefresh: Boolean) {
96 | viewModelScope.launch {
97 | runCatching {
98 | getSearchDataUseCase.execute(
99 | searchUiState.value.searchWord,
100 | pageCount.getAndIncrement(),
101 | )
102 | }.onSuccess {
103 | if (it.items.isNotEmpty()) {
104 | updateUiState(isRefresh, it)
105 | } else {
106 | _searchUiState.update { uiState ->
107 | when (val loadState = uiState.searchState) {
108 | is LoadState.Loaded -> {
109 | val searchState = when (val state = loadState.data) {
110 | is SearchState.Data -> state.copy(
111 | hasMore = false
112 | )
113 | else -> SearchState.Empty
114 | }
115 | uiState.copy(searchState = loadState.copy(data = searchState))
116 | }
117 | else -> uiState
118 | }
119 | }
120 | }
121 | }.onFailure {
122 | _searchUiState.update { uiState ->
123 | when (uiState.searchState) {
124 | is LoadState.Loaded -> {
125 | uiState.copy(errors = uiState.errors.plus(it))
126 | }
127 | else -> {
128 | uiState.copy(searchState = LoadState.Error("Error"))
129 | }
130 | }
131 | }
132 | }
133 | isLoadingMore.set(false)
134 | }
135 | }
136 |
137 | private fun updateUiState(isRefresh: Boolean, search: Search) {
138 | val hasMore = search.totalCount > PER_PAGE * pageCount.get()
139 | _searchUiState.update { uiState ->
140 | when (val loadState = uiState.searchState) {
141 | is LoadState.Loaded -> {
142 | when (val searchState = loadState.data) {
143 | is SearchState.Data -> {
144 | val repositoryList = if (isRefresh) {
145 | search.items
146 | } else {
147 | searchState.repositoryList + search.items
148 | }
149 | uiState.copy(
150 | searchState = loadState.copy(
151 | data = searchState.copy(
152 | repositoryList = repositoryList,
153 | hasMore = hasMore
154 | )
155 | )
156 | )
157 | }
158 | else -> {
159 | uiState.copy(
160 | searchState = loadState.copy(
161 | data = SearchState.Data(
162 | repositoryList = search.items,
163 | hasMore = hasMore
164 | )
165 | )
166 | )
167 | }
168 | }
169 | }
170 | else -> {
171 | uiState.copy(
172 | searchState = LoadState.Loaded(
173 | data = SearchState.Data(
174 | repositoryList = search.items,
175 | hasMore = hasMore
176 | )
177 | )
178 | )
179 | }
180 | }
181 | }
182 | }
183 |
184 | companion object {
185 | private const val PER_PAGE = 30
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/ui/viewModel/src/test/java/jp/dosukoi/ui/viewmodel/myPage/MyPageViewModelUnitTest.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.viewmodel.myPage
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import io.mockk.MockKAnnotations
5 | import io.mockk.coEvery
6 | import io.mockk.impl.annotations.InjectMockKs
7 | import io.mockk.impl.annotations.RelaxedMockK
8 | import io.mockk.mockk
9 | import jp.dosukoi.githubclient.domain.entity.myPage.UserStatus
10 | import jp.dosukoi.githubclient.domain.usecase.auth.GetAccessTokenUseCase
11 | import jp.dosukoi.githubclient.domain.usecase.myPage.GetRepositoriesUseCase
12 | import jp.dosukoi.githubclient.domain.usecase.myPage.GetUserStatusUseCase
13 | import jp.dosukoi.testing.common.assertType
14 | import jp.dosukoi.testing.common.testRule
15 | import jp.dosukoi.ui.viewmodel.common.LoadState
16 | import kotlinx.coroutines.ExperimentalCoroutinesApi
17 | import kotlinx.coroutines.flow.launchIn
18 | import kotlinx.coroutines.flow.onEach
19 | import kotlinx.coroutines.test.runBlockingTest
20 | import org.junit.Before
21 | import org.junit.Rule
22 | import org.junit.Test
23 |
24 | @OptIn(ExperimentalCoroutinesApi::class)
25 | class MyPageViewModelUnitTest {
26 |
27 | @get:Rule
28 | val rule = testRule()
29 |
30 | @RelaxedMockK
31 | private lateinit var getUserStatusUseCase: GetUserStatusUseCase
32 |
33 | @RelaxedMockK
34 | private lateinit var getRepositoriesUseCase: GetRepositoriesUseCase
35 |
36 | @RelaxedMockK
37 | private lateinit var getAccessTokenUseCase: GetAccessTokenUseCase
38 |
39 | @InjectMockKs
40 | private lateinit var viewModel: MyPageViewModel
41 |
42 | @Before
43 | fun setup() {
44 | MockKAnnotations.init(this)
45 | coEvery { getUserStatusUseCase.execute() } returns UserStatus.Authenticated(mockk())
46 | coEvery { getRepositoriesUseCase.execute() } returns listOf(
47 | mockk(),
48 | mockk(),
49 | mockk()
50 | )
51 | }
52 |
53 | @Test
54 | fun init_success_authenticated() = runBlockingTest {
55 | // given
56 | val myPageState = mutableListOf()
57 | val job = viewModel.myPageState.onEach {
58 | myPageState.add(it)
59 | }.launchIn(this)
60 |
61 | // when
62 | viewModel.init()
63 |
64 | // then
65 | assertThat(myPageState[0].screenState).isEqualTo(LoadState.Loading)
66 | assertType>(myPageState[1].screenState) {
67 | assertThat(this.data.userStatus).isInstanceOf(UserStatus.Authenticated::class.java)
68 | assertThat(this.data.repositoryList.size).isEqualTo(3)
69 | }
70 | job.cancel()
71 | }
72 |
73 | @Test
74 | fun init_success_un_authenticated() = runBlockingTest {
75 | // given
76 | coEvery { getUserStatusUseCase.execute() } returns UserStatus.UnAuthenticated
77 | val myPageState = mutableListOf()
78 | val job = viewModel.myPageState.onEach {
79 | myPageState.add(it)
80 | }.launchIn(this)
81 |
82 | // when
83 | viewModel.init()
84 |
85 | // then
86 | assertThat(myPageState[0].screenState).isEqualTo(LoadState.Loading)
87 | assertType>(myPageState[1].screenState) {
88 | assertThat(this.data.userStatus).isEqualTo(UserStatus.UnAuthenticated)
89 | }
90 | job.cancel()
91 | }
92 |
93 | @Test
94 | fun init_failure() = runBlockingTest {
95 | // given
96 | coEvery { getUserStatusUseCase.execute() } throws RuntimeException()
97 | val myPageState = mutableListOf()
98 | val job = viewModel.myPageState.onEach {
99 | myPageState.add(it)
100 | }.launchIn(this)
101 |
102 | // when
103 | viewModel.init()
104 |
105 | // then
106 | assertThat(myPageState[0].screenState).isEqualTo(LoadState.Loading)
107 | assertThat(myPageState[1].screenState).isInstanceOf(LoadState.Error::class.java)
108 |
109 | job.cancel()
110 | }
111 |
112 | @Test
113 | fun onRetryClick() = runBlockingTest {
114 | // given
115 | val myPageState = mutableListOf()
116 | val job = viewModel.myPageState.onEach {
117 | myPageState.add(it)
118 | }.launchIn(this)
119 |
120 | // when
121 | viewModel.onRetryClick()
122 |
123 | // then
124 | assertThat(myPageState[0].screenState).isEqualTo(LoadState.Loading)
125 | assertType>(myPageState[1].screenState) {
126 | assertThat(this.data.repositoryList.size).isEqualTo(3)
127 | }
128 | job.cancel()
129 | }
130 |
131 | @Test
132 | fun onRefresh() = runBlockingTest {
133 | // given
134 | coEvery { getRepositoriesUseCase.execute() } returnsMany listOf(
135 | listOf(
136 | mockk(),
137 | mockk(),
138 | mockk()
139 | ),
140 | listOf(
141 | mockk(),
142 | mockk(),
143 | mockk(),
144 | mockk()
145 | )
146 | )
147 | val myPageState = mutableListOf()
148 | val job = viewModel.myPageState.onEach {
149 | myPageState.add(it)
150 | }.launchIn(this)
151 |
152 | // when
153 | viewModel.init()
154 | viewModel.onRefresh()
155 |
156 | // then
157 | assertThat(myPageState[0].screenState).isEqualTo(LoadState.Loading)
158 | assertType>(myPageState[1].screenState) {
159 | assertThat(this.data.repositoryList.size).isEqualTo(3)
160 | }
161 | assertThat(myPageState[2].isRefreshing).isTrue()
162 | myPageState[3].also {
163 | assertThat(it.isRefreshing).isFalse()
164 | assertType>(it.screenState) {
165 | assertThat(this.data.repositoryList.size).isEqualTo(4)
166 | }
167 | }
168 |
169 | job.cancel()
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/ui/viewModel/src/test/java/jp/dosukoi/ui/viewmodel/search/SearchViewModelUnitTest.kt:
--------------------------------------------------------------------------------
1 | package jp.dosukoi.ui.viewmodel.search
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import io.mockk.MockKAnnotations
5 | import io.mockk.coEvery
6 | import io.mockk.impl.annotations.InjectMockKs
7 | import io.mockk.impl.annotations.RelaxedMockK
8 | import io.mockk.mockk
9 | import jp.dosukoi.githubclient.domain.entity.search.Search
10 | import jp.dosukoi.githubclient.domain.usecase.search.GetSearchDataUseCase
11 | import jp.dosukoi.testing.common.testRule
12 | import kotlinx.coroutines.ExperimentalCoroutinesApi
13 | import kotlinx.coroutines.flow.launchIn
14 | import kotlinx.coroutines.flow.onEach
15 | import kotlinx.coroutines.test.runBlockingTest
16 | import org.junit.Before
17 | import org.junit.Rule
18 | import org.junit.Test
19 |
20 | @OptIn(ExperimentalCoroutinesApi::class)
21 | class SearchViewModelUnitTest {
22 |
23 | @get:Rule
24 | val rule = testRule()
25 |
26 | @RelaxedMockK
27 | private lateinit var getSearchDataUseCase: GetSearchDataUseCase
28 |
29 | @InjectMockKs
30 | private lateinit var viewModel: SearchViewModel
31 |
32 | @Before
33 | fun setup() {
34 | MockKAnnotations.init(this)
35 | coEvery { getSearchDataUseCase.execute(any(), any()) } returns Search(
36 | totalCount = 3,
37 | incompleteResults = true,
38 | items = listOf(
39 | mockk(),
40 | mockk(),
41 | mockk()
42 | )
43 | )
44 | }
45 |
46 | @Test
47 | fun onSearchWordChanged() = runBlockingTest {
48 | // given
49 | val searchUiState = mutableListOf()
50 | val job = viewModel.searchUiState.onEach {
51 | searchUiState.add(it)
52 | }.launchIn(this)
53 |
54 | // when
55 | viewModel.onSearchWordChanged("kotlin")
56 | viewModel.onSearchWordChanged("java")
57 |
58 | // then
59 | assertThat(searchUiState[1].searchWord).isEqualTo("kotlin")
60 | assertThat(searchUiState[2].searchWord).isEqualTo("java")
61 | job.cancel()
62 | }
63 | }
64 |
--------------------------------------------------------------------------------