├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml └── misc.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── jetpackcompose │ │ └── github │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── jetpackcompose │ │ │ └── github │ │ │ ├── GitHubApplication.kt │ │ │ ├── model │ │ │ ├── remote_data_source │ │ │ │ ├── ApiClient.kt │ │ │ │ ├── ApiClientModule.kt │ │ │ │ ├── ApiClientProvider.kt │ │ │ │ ├── GitHubUser.kt │ │ │ │ ├── RemoteDataSource.kt │ │ │ │ ├── RemoteDataSourceImpl.kt │ │ │ │ └── RemoteDataSourceModule.kt │ │ │ └── repository │ │ │ │ ├── NetworkImage.kt │ │ │ │ ├── Url.kt │ │ │ │ ├── User.kt │ │ │ │ ├── UserId.kt │ │ │ │ ├── UserRepository.kt │ │ │ │ ├── UserRepositoryImpl.kt │ │ │ │ └── UserRepositoryModule.kt │ │ │ ├── view │ │ │ ├── ErrorView.kt │ │ │ ├── InitialView.kt │ │ │ ├── LoadingView.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MainView.kt │ │ │ ├── SearchView.kt │ │ │ └── UserDetailView.kt │ │ │ └── viewmodel │ │ │ └── MainViewModel.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── example │ └── jetpackcompose │ └── github │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | jetpackcompose github -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 「Jetpack ComposeによるAndroid MVVMアーキテクチャ入門」サンプルアプリ 2 | 「Jetpack ComposeによるAndroid MVVMアーキテクチャ入門」第4章のサンプルアプリのリポジトリです。 3 | 4 | ## ブランチについて 5 | 本書執筆時点からAndroid StudioやComposeにアップデートがあります。それらのアップデートに追従したブランチを作成してあります。 `main` ブランチのコードは、本書執筆時のコードになっています。 6 | 7 | ### Arctic Fox 2020.3.1 Patch 2、Compose 1.0.2 8 | 以下の環境に対応したブランチです。 9 | 10 | - Android Studio Arctic Fox | 2020.3.1 Patch 2 11 | - Compose 1.0.2 12 | 13 | [GitHub \- okuzawats/sample\-android\-mvvm\-with\-jetpack\-compose\-github\-client\-app at arctic\-fox\-2020\.3\.1\-patch\-2\-compose\-1\.0\.2](https://github.com/okuzawats/sample-android-mvvm-with-jetpack-compose-github-client-app/tree/arctic-fox-2020.3.1-patch-2-compose-1.0.2) 14 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | id 'kotlinx-serialization' 6 | id 'dagger.hilt.android.plugin' 7 | } 8 | 9 | android { 10 | compileSdk 30 11 | buildToolsVersion "30.0.3" 12 | 13 | defaultConfig { 14 | applicationId "com.example.jetpackcompose.github" 15 | minSdk 21 16 | targetSdk 30 17 | versionCode 1 18 | versionName "1.0" 19 | 20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 21 | vectorDrawables { 22 | useSupportLibrary true 23 | } 24 | } 25 | 26 | buildTypes { 27 | release { 28 | minifyEnabled false 29 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 30 | } 31 | } 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | useIR = true 39 | } 40 | buildFeatures { 41 | compose true 42 | } 43 | composeOptions { 44 | kotlinCompilerExtensionVersion compose_version 45 | kotlinCompilerVersion '1.4.32' 46 | } 47 | } 48 | 49 | dependencies { 50 | 51 | implementation 'androidx.core:core-ktx:1.3.2' 52 | implementation 'androidx.appcompat:appcompat:1.2.0' 53 | implementation 'com.google.android.material:material:1.3.0' 54 | implementation "androidx.compose.ui:ui:$compose_version" 55 | implementation "androidx.compose.material:material:$compose_version" 56 | implementation "androidx.compose.ui:ui-tooling:$compose_version" 57 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' 58 | implementation 'androidx.activity:activity-compose:1.3.0-alpha07' 59 | implementation "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha04" 60 | implementation "com.squareup.okhttp3:okhttp:4.9.0" 61 | implementation "com.squareup.retrofit2:retrofit:2.9.0" 62 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.0" 63 | implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" 64 | implementation "com.google.dagger:hilt-android:2.35" 65 | kapt "com.google.dagger:hilt-compiler:2.35" 66 | testImplementation 'junit:junit:4.+' 67 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 68 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 69 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 70 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/jetpackcompose/github/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.example.jetpackcompose.github", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/GitHubApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | /** 7 | * 独自のApplicationクラス 8 | * 9 | * Hiltの @HiltAndroidApp を付与するために作成 10 | */ 11 | @HiltAndroidApp 12 | class GitHubApplication : Application() -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/model/remote_data_source/ApiClient.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.model.remote_data_source 2 | 3 | import retrofit2.Response 4 | import retrofit2.http.GET 5 | import retrofit2.http.Path 6 | 7 | /** 8 | * Retrofitを用いたGitHub APIのクライアント 9 | */ 10 | interface ApiClient { 11 | @GET("users/{username}") 12 | suspend fun getGitHubUser(@Path("username") userName: String): Response 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/model/remote_data_source/ApiClientModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.model.remote_data_source 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import kotlinx.serialization.ExperimentalSerializationApi 8 | import javax.inject.Singleton 9 | 10 | /** 11 | * [ApiClient]のModule 12 | */ 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | class ApiClientModule { 16 | 17 | /** 18 | * [ApiClient]のDIに用いられるインスタンスを生成して返す 19 | */ 20 | @ExperimentalSerializationApi 21 | @Provides 22 | @Singleton 23 | fun provideApiClient(apiClientProvider: ApiClientProvider): ApiClient { 24 | return apiClientProvider.provide() 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/model/remote_data_source/ApiClientProvider.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.model.remote_data_source 2 | 3 | import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory 4 | import kotlinx.serialization.ExperimentalSerializationApi 5 | import kotlinx.serialization.json.Json 6 | import okhttp3.MediaType.Companion.toMediaType 7 | import retrofit2.Retrofit 8 | import javax.inject.Inject 9 | 10 | /** 11 | * [ApiClient]を生成するクラス 12 | */ 13 | class ApiClientProvider @Inject constructor() { 14 | companion object { 15 | private const val API_END_POINT = "https://api.github.com/" 16 | } 17 | 18 | /** 19 | * [ApiClient]を返す 20 | */ 21 | @ExperimentalSerializationApi 22 | fun provide(): ApiClient { 23 | return Retrofit.Builder() 24 | .baseUrl(API_END_POINT) 25 | .addConverterFactory( 26 | Json { 27 | ignoreUnknownKeys = true 28 | }.asConverterFactory( 29 | "application/json".toMediaType() 30 | ), 31 | ) 32 | .build() 33 | .create(ApiClient::class.java) 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/model/remote_data_source/GitHubUser.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.model.remote_data_source 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * GitHub APIからのレスポンス 8 | */ 9 | @Serializable 10 | data class GitHubUser( 11 | @SerialName("id") val id: Long, 12 | @SerialName("name") val name: String, 13 | @SerialName("avatar_url") val avatarUrl: String, 14 | @SerialName("blog") val blog: String, 15 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/model/remote_data_source/RemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.model.remote_data_source 2 | 3 | /** 4 | * サーバーからのレスポンスを取り出すData Source 5 | */ 6 | interface RemoteDataSource { 7 | 8 | /** 9 | * サーバーからのレスポンスを[GitHubUser]として返す 10 | */ 11 | suspend fun getGitHubUser(userName: String): GitHubUser 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/model/remote_data_source/RemoteDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.model.remote_data_source 2 | 3 | import javax.inject.Inject 4 | 5 | /** 6 | * [RemoteDataSource]の実装クラス 7 | */ 8 | class RemoteDataSourceImpl @Inject constructor( 9 | private val apiClient: ApiClient, 10 | ) : RemoteDataSource { 11 | override suspend fun getGitHubUser(userName: String): GitHubUser { 12 | val response = apiClient.getGitHubUser(userName = userName) 13 | if (response.isSuccessful) { 14 | val gitHubUser: GitHubUser = requireNotNull(response.body()) 15 | return gitHubUser 16 | } 17 | throw HttpException() 18 | } 19 | } 20 | 21 | /** 22 | * ResponseがisSuccessful != trueだった時に投げられる例外 23 | */ 24 | class HttpException : Throwable() -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/model/remote_data_source/RemoteDataSourceModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.model.remote_data_source 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.android.components.ViewModelComponent 7 | 8 | /** 9 | * [RemoteDataSource]のModule 10 | */ 11 | @Module 12 | @InstallIn(ViewModelComponent::class) 13 | class RemoteDataSourceModule { 14 | 15 | /** 16 | * [RemoteDataSource]のDIに用いられるインスタンスを生成して返す 17 | */ 18 | @Provides 19 | fun provideRemoteDataSource(remoteDataSourceImpl: RemoteDataSourceImpl): RemoteDataSource { 20 | return remoteDataSourceImpl 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/model/repository/NetworkImage.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.model.repository 2 | 3 | /** 4 | * ネットワークから読み込む画像を表すクラス 5 | */ 6 | data class NetworkImage( 7 | val url: Url, 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/model/repository/Url.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.model.repository 2 | 3 | /** 4 | * URLを表すクラス 5 | */ 6 | data class Url( 7 | val value: String, 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/model/repository/User.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.model.repository 2 | 3 | /** 4 | * ユーザーを表すクラス 5 | */ 6 | data class User( 7 | val userId: UserId, 8 | val name: String, 9 | val avatarImage: NetworkImage, 10 | val blogUrl: Url, 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/model/repository/UserId.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.model.repository 2 | 3 | /** 4 | * ユーザーのIDを表すクラス 5 | */ 6 | data class UserId( 7 | val value: Long, 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/model/repository/UserRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.model.repository 2 | 3 | /** 4 | * [User]のRepository 5 | */ 6 | interface UserRepository { 7 | 8 | /** 9 | * [userName]に該当する[User]を返す 10 | */ 11 | suspend fun getUser(userName: String): User 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/model/repository/UserRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.model.repository 2 | 3 | import com.example.jetpackcompose.github.model.remote_data_source.GitHubUser 4 | import com.example.jetpackcompose.github.model.remote_data_source.RemoteDataSource 5 | import javax.inject.Inject 6 | 7 | /** 8 | * [UserRepository]の実装クラス 9 | */ 10 | class UserRepositoryImpl @Inject constructor( 11 | private val remoteDataSource: RemoteDataSource, 12 | ) : UserRepository { 13 | override suspend fun getUser(userName: String): User { 14 | return remoteDataSource.getGitHubUser(userName = userName).toUser() 15 | } 16 | } 17 | 18 | // [GitHubUser]を[User]に変換する拡張関数 19 | private fun GitHubUser.toUser(): User { 20 | return User( 21 | userId = UserId(value = id), 22 | name = name, 23 | avatarImage = NetworkImage(url = Url(value = avatarUrl)), 24 | blogUrl = Url(value = blog) 25 | ) 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/model/repository/UserRepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.model.repository 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.android.components.ViewModelComponent 7 | 8 | /** 9 | * [UserRepository]のModule 10 | */ 11 | @Module 12 | @InstallIn(ViewModelComponent::class) 13 | class UserRepositoryModule { 14 | 15 | /** 16 | * [UserRepository]のDIに用いられるインスタンスを生成して返す 17 | */ 18 | @Provides 19 | fun provideUserRepository(userRepositoryImpl: UserRepositoryImpl): UserRepository { 20 | return userRepositoryImpl 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/view/ErrorView.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.view 2 | 3 | import androidx.compose.material.Text 4 | import androidx.compose.runtime.Composable 5 | 6 | /** 7 | * エラー表示 8 | */ 9 | @Composable 10 | fun ErrorView() { 11 | Text( 12 | text = "読み込み失敗" 13 | ) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/view/InitialView.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.view 2 | 3 | import androidx.compose.material.Text 4 | import androidx.compose.runtime.Composable 5 | 6 | /** 7 | * 初期表示 8 | */ 9 | @Composable 10 | fun InitialView() { 11 | Text( 12 | text = "検索してください" 13 | ) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/view/LoadingView.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.view 2 | 3 | import androidx.compose.material.Text 4 | import androidx.compose.runtime.Composable 5 | 6 | /** 7 | * 読み込み中表示 8 | */ 9 | @Composable 10 | fun LoadingView() { 11 | Text( 12 | text = "読み込み中" 13 | ) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/view/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.view 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.viewModels 7 | import com.example.jetpackcompose.github.viewmodel.MainViewModel 8 | import dagger.hilt.android.AndroidEntryPoint 9 | 10 | @AndroidEntryPoint 11 | class MainActivity : ComponentActivity() { 12 | private val mainViewModel: MainViewModel by viewModels() 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | setContent { 17 | MainView(mainViewModel = mainViewModel) 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/view/MainView.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.view 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.ui.Modifier 8 | import com.example.jetpackcompose.github.model.repository.User 9 | import com.example.jetpackcompose.github.viewmodel.MainViewModel 10 | 11 | /** 12 | * メイン画面 13 | * 14 | * ViewModelの状態に応じて、初期表示、読み込み中表示、ユーザ詳細表示、エラー表示を切り替えて表示する 15 | */ 16 | @Composable 17 | fun MainView(mainViewModel: MainViewModel) { 18 | val uiState: MainViewModel.UiState by mainViewModel.uiState 19 | 20 | Column(Modifier.fillMaxWidth()) { 21 | SearchView( 22 | searchQuery = mainViewModel.searchQuery, 23 | onSearchButtonTapped = mainViewModel::onSearchTapped, 24 | ) 25 | when (uiState) { 26 | is MainViewModel.UiState.Initial -> { 27 | InitialView() 28 | } 29 | is MainViewModel.UiState.Loading -> { 30 | LoadingView() 31 | } 32 | is MainViewModel.UiState.Success -> { 33 | UserDetailView(user = uiState.requireUser()) 34 | } 35 | is MainViewModel.UiState.Failure -> { 36 | ErrorView() 37 | } 38 | } 39 | } 40 | } 41 | 42 | // MainViewModelが保持するUserを強制的に取り出す 43 | private fun MainViewModel.UiState.requireUser(): User { 44 | if (this !is MainViewModel.UiState.Success) throw IllegalStateException("user is not loaded") 45 | return user 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/view/SearchView.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.view 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.material.Button 6 | import androidx.compose.material.Text 7 | import androidx.compose.material.TextField 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.MutableState 10 | import androidx.compose.ui.Modifier 11 | 12 | /** 13 | * 検索キーワードの入力フォームと検索実行ボタンの表示 14 | */ 15 | @Composable 16 | fun SearchView( 17 | searchQuery: MutableState, 18 | onSearchButtonTapped: () -> Unit 19 | ) { 20 | Row(Modifier.fillMaxWidth()) { 21 | TextField( 22 | label = { 23 | Text("GitHubのアカウントを入力") 24 | }, 25 | value = searchQuery.value, 26 | onValueChange = { text -> 27 | searchQuery.value = text 28 | }, 29 | modifier = Modifier.weight(1f) 30 | ) 31 | Button( 32 | onClick = { 33 | onSearchButtonTapped() 34 | } 35 | ) { 36 | Text( 37 | text = "検索" 38 | ) 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/view/UserDetailView.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.view 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.material.Text 5 | import androidx.compose.runtime.Composable 6 | import com.example.jetpackcompose.github.model.repository.User 7 | 8 | @Composable 9 | fun UserDetailView(user: User) { 10 | Column { 11 | Text( 12 | text = user.userId.value.toString() 13 | ) 14 | Text( 15 | text = user.name 16 | ) 17 | Text( 18 | text = user.avatarImage.url.value 19 | ) 20 | Text( 21 | text = user.blogUrl.value 22 | ) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/jetpackcompose/github/viewmodel/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.jetpackcompose.github.viewmodel 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.example.jetpackcompose.github.model.repository.User 8 | import com.example.jetpackcompose.github.model.repository.UserRepository 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.launch 11 | import javax.inject.Inject 12 | 13 | /** 14 | * メイン画面に対するViewModel 15 | */ 16 | @HiltViewModel 17 | class MainViewModel @Inject constructor( 18 | private val userRepository: UserRepository 19 | ) : ViewModel() { 20 | 21 | /** 22 | * Viewの状態を表すsealed class 23 | */ 24 | sealed class UiState { 25 | 26 | /** 27 | * 初期状態 28 | */ 29 | object Initial : UiState() 30 | 31 | /** 32 | * 読み込み中 33 | */ 34 | object Loading : UiState() 35 | 36 | /** 37 | * 読み込み成功 38 | */ 39 | data class Success(val user: User) : UiState() 40 | 41 | /** 42 | * 読み込み失敗 43 | */ 44 | object Failure : UiState() 45 | } 46 | 47 | /** 48 | * Viewの状態を[UiState]として表すMutableState 49 | */ 50 | val uiState: MutableState = mutableStateOf(UiState.Initial) 51 | 52 | /** 53 | * 検索フォームに入力された文字列を表すMutableState 54 | */ 55 | val searchQuery: MutableState = mutableStateOf("") 56 | 57 | /** 58 | * 検索を実行する。 59 | * 60 | * searchQueryから検索フォームに入力された文字列を取得し、 61 | * Repositoryを経由してユーザを問い合わせる。 62 | */ 63 | fun onSearchTapped() { 64 | val searchQuery: String = searchQuery.value 65 | 66 | viewModelScope.launch { 67 | uiState.value = UiState.Loading 68 | runCatching { 69 | userRepository.getUser(userName = searchQuery) 70 | }.onSuccess { 71 | uiState.value = UiState.Success(user = it) 72 | }.onFailure { 73 | uiState.value = UiState.Failure 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /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/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/okuzawats/sample-android-mvvm-with-jetpack-compose-github-client-app/f77bc095c8f6a40c3b5bad7c911b7b1ab47540d0/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okuzawats/sample-android-mvvm-with-jetpack-compose-github-client-app/f77bc095c8f6a40c3b5bad7c911b7b1ab47540d0/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okuzawats/sample-android-mvvm-with-jetpack-compose-github-client-app/f77bc095c8f6a40c3b5bad7c911b7b1ab47540d0/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okuzawats/sample-android-mvvm-with-jetpack-compose-github-client-app/f77bc095c8f6a40c3b5bad7c911b7b1ab47540d0/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okuzawats/sample-android-mvvm-with-jetpack-compose-github-client-app/f77bc095c8f6a40c3b5bad7c911b7b1ab47540d0/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okuzawats/sample-android-mvvm-with-jetpack-compose-github-client-app/f77bc095c8f6a40c3b5bad7c911b7b1ab47540d0/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okuzawats/sample-android-mvvm-with-jetpack-compose-github-client-app/f77bc095c8f6a40c3b5bad7c911b7b1ab47540d0/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okuzawats/sample-android-mvvm-with-jetpack-compose-github-client-app/f77bc095c8f6a40c3b5bad7c911b7b1ab47540d0/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okuzawats/sample-android-mvvm-with-jetpack-compose-github-client-app/f77bc095c8f6a40c3b5bad7c911b7b1ab47540d0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okuzawats/sample-android-mvvm-with-jetpack-compose-github-client-app/f77bc095c8f6a40c3b5bad7c911b7b1ab47540d0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /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 | jetpackcompose github 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 21 | 22 |