├── .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 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/saveactions_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![project dependencies](gradle/dependency-graph/project.dot.png) 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 | --------------------------------------------------------------------------------