Skip to content

Step3 : Github(UI) #99

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ plugins {
id("com.android.application")
kotlin("android")
kotlin("kapt")
id("com.google.dagger.hilt.android")
id("org.jlleitschuh.gradle.ktlint") version "11.5.0"
}

android {
compileSdk = Version.compileSdk

defaultConfig {
applicationId = "edu.nextstep.camp.calculator"
applicationId = "edu.nextstep.camp.github"
minSdk = Version.minSdk
targetSdk = Version.targetSdk
versionCode = 1
Expand Down Expand Up @@ -39,18 +40,51 @@ android {
buildFeatures {
viewBinding = true
dataBinding = true
compose = true
}

composeOptions {
kotlinCompilerExtensionVersion = Version.composeCompiler
}
}

dependencies {
val composeBom = platform("androidx.compose:compose-bom:2023.01.00")
implementation(composeBom)
androidTestImplementation(composeBom)

// Choose one of the following:
// Material Design 3
implementation("androidx.compose.material3:material3")
// such as input and measurement/layout
implementation("androidx.compose.ui:ui")

// Optional - Integration with activities
implementation("androidx.activity:activity-compose:1.7.2")
// Optional - Integration with ViewModels
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
// Optional - Integration with LiveData
implementation("androidx.compose.runtime:runtime-livedata")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")

implementation("org.jetbrains.kotlin:kotlin-stdlib:${Version.kotlin}")
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.0")
implementation("com.google.android.material:material:1.7.0")
implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.9.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.fragment:fragment-ktx:1.5.5")
implementation("androidx.fragment:fragment-ktx:1.6.1")

implementation("com.google.dagger:hilt-android:2.47")
kapt("com.google.dagger:hilt-android-compiler:2.47")

implementation("org.orbit-mvi:orbit-core:${Version.orbit}")
implementation("org.orbit-mvi:orbit-viewmodel:${Version.orbit}")
implementation("org.orbit-mvi:orbit-compose:${Version.orbit}")

implementation(project(":core:data"))
implementation(project(":core:domain"))

testImplementation("org.orbit-mvi:orbit-test:${Version.orbit}")
testImplementation("junit:junit:4.13.2")
testImplementation("com.google.truth:truth:1.1.4")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="camp.nextstep.edu.github">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:name=".GithubApplication"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Androidgithub">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package camp.nextstep.edu.github

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class GithubApplication : Application()
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package camp.nextstep.edu.github

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import camp.nextstep.edu.github.domain.network.GithubDataSource
import camp.nextstep.edu.github.ui.UiStatus
import camp.nextstep.edu.github.ui.main.GithubMainSideEffect
import camp.nextstep.edu.github.ui.main.GithubMainState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.syntax.simple.intent
import org.orbitmvi.orbit.syntax.simple.reduce
import org.orbitmvi.orbit.viewmodel.container
import javax.inject.Inject

@HiltViewModel
class GithubRepositoriesViewModel @Inject constructor(
private val dataSource: GithubDataSource
) : ContainerHost<GithubMainState, GithubMainSideEffect>, ViewModel() {
override val container = container<GithubMainState, GithubMainSideEffect>(GithubMainState())

init {
fetchRepositories()
}

private fun fetchRepositories() {
intent {
reduce { state.copy(status = UiStatus.Loading) }
viewModelScope.launch {
runCatching {
val repositories = dataSource.fetchRepositories()
reduce {
state.copy(
status = UiStatus.Success,
repositories = repositories
)
}
}.getOrElse {
reduce { state.copy(status = UiStatus.Failed("Error : ${it.message}")) }
}
}
}
}
}
40 changes: 39 additions & 1 deletion app/src/main/java/camp/nextstep/edu/github/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,49 @@
package camp.nextstep.edu.github

import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import camp.nextstep.edu.github.theme.GitHubMainTheme
import camp.nextstep.edu.github.ui.main.component.GithubRepositoryPage
import dagger.hilt.android.AndroidEntryPoint
import org.orbitmvi.orbit.compose.collectAsState
import org.orbitmvi.orbit.compose.collectSideEffect

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: GithubRepositoriesViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

setContent {
GitHubMainTheme {
Box(modifier = Modifier.fillMaxSize()) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "Main") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compose 네비게이션을 활용해서 구성해주셨군요! 지금은 Main 네비게이션 하나밖에 없지만, 나중에 앱이 커지게 되면 네비게이션 Route를 적절하게 관리할 방법을 찾고 싶어질 수 있어요.

NowInAndroid에서 네비게이션을 어떻게 관리하는지 보시면 도움이 될 것 같습니다.

https://github.com/android/nowinandroid

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정보 감사합니다. 사실 해당 mission이 Main밖에 없어 이 부분에 대해서 더 보지 못하였는데요. 참고로 주신 리파지토리 보도록 하겠습니다.

addMain(navController)
}
}
}
}
}

private fun NavGraphBuilder.addMain(navController: NavHostController) {
composable(route = "Main") {
val state by viewModel.collectAsState()
viewModel.collectSideEffect {
navController.navigate(route = "Main")
}
GithubRepositoryPage(state)
}
}
}
11 changes: 11 additions & 0 deletions app/src/main/java/camp/nextstep/edu/github/theme/Theme.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package camp.nextstep.edu.github.theme

import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable

@Composable
fun GitHubMainTheme(content: @Composable () -> Unit) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 현재는 MaterialTheme을 그대로 사용하셔도 무방합니다. 컬러, 폰트 등 프리셋이 생기면 그때 추가해도 괜찮아보여요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원래 프리셋 등으로 해서 적용해보는 식으로 할까 하다가 다른 속성에 대해서 적용을 해볼까 하다가 말았는데요. 현업에서 MaterialTheme보다는 커스텀한 상황을 많이 쓰겠죠? 제가 컴포즈를 잘 몰라 문의드립니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현업에서는 디자인 시스템 프리셋을 정의하는 경우가 많고, MaterialTheme을 그대로 따라가기에는 아이폰과 같은 디자인인 경우가 많아서 대부분 커스텀으로 정의하긴 합니다. 현재 미션과는 크게 관련 없으니 참고만 해주세요!

MaterialTheme(
content = content
)
}
8 changes: 8 additions & 0 deletions app/src/main/java/camp/nextstep/edu/github/ui/UiStatus.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package camp.nextstep.edu.github.ui

sealed class UiStatus {

object Loading : UiStatus()
object Success : UiStatus()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

단순 성공 / 실패 상태만 명시하기보다 이 UI 자체에 데이터의 흐름을 담아보면 어떨까요?

사용하신 MVI 라이브러리에 대한 이해도가 낮은 상태라 적절하지 않은 피드백일 수 있습니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재는 state는 명시적으로 로딩 중과 성공 실패에대한 상태를 넘기고 데이터의 경우 추가적으로 넘길 수 있도록 구현하였는데요. Status 자체에 데이터를 넘기는 식으로 하는게 더 좋을까요?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떤 것이 더 좋다라고 이야기하기보다 UI 상태 자체에 데이터를 포함시킬 수 있단 의미입니다. 예를 들어 로딩 상태일때는 별다른 데이터를 받지 않아도 되지만, 성공 상태일때는 레포지토리 정보를 받는 것을 보장할 수 있습니다.

다음 자료들이 도움이 되면 좋겠어요!

참고로 이번 미션 학습 목표와는 거리가 있으니 꼭 반영하진 않으셔도 됩니다.

data class Failed(val message: String) : UiStatus()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package camp.nextstep.edu.github.ui.main

sealed class GithubMainSideEffect {
object Completed : GithubMainSideEffect()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package camp.nextstep.edu.github.ui.main

import camp.nextstep.edu.github.domain.GithubRepository
import camp.nextstep.edu.github.ui.UiStatus

data class GithubMainState(
val status: UiStatus? = null,
val repositories: List<GithubRepository> = emptyList()
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package camp.nextstep.edu.github.ui.main.component

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import camp.nextstep.edu.github.domain.GithubRepository
import camp.nextstep.edu.github.ui.UiStatus
import camp.nextstep.edu.github.ui.main.GithubMainState

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GithubRepositoryPage(
status: GithubMainState
) {
if (status.status == UiStatus.Loading) {
ProgressLoadingView()
}
if (status.status == UiStatus.Success) {
Scaffold {
Box(
modifier = Modifier
.fillMaxSize()
.padding(it)
) {
RepositoriesView(status)
}
}
}
}

@Composable
private fun ProgressLoadingView() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}

@Composable
fun RepositoriesView(
status: GithubMainState
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(10.dp),
contentPadding = PaddingValues(10.dp)
) {
items(status.repositories) {
RepositoryItemView(it)
}
}
}

@Composable
fun RepositoryItemView(repository: GithubRepository) {
Column {
Text(text = repository.fullName, style = MaterialTheme.typography.displayMedium)
Text(text = repository.description ?: "", style = MaterialTheme.typography.bodyMedium)
Divider(color = Color.Black)
}
}
5 changes: 5 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ buildscript {
}
}

plugins {
id("com.google.devtools.ksp") version "1.8.10-1.0.9" apply false
id("com.google.dagger.hilt.android") version "2.47" apply false
}

allprojects {
repositories {
google()
Expand Down
6 changes: 4 additions & 2 deletions buildSrc/src/main/kotlin/Configs.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
object Version {
const val androidGradlePlugin = "7.4.0"
const val kotlin = "1.9.0"
const val androidGradlePlugin = "7.4.2"
const val kotlin = "1.8.10"
const val compileSdk = 33
const val minSdk = 26
const val targetSdk = 33
const val retrofit = "2.9.0"
const val orbit = "6.0.0"
const val composeCompiler = "1.4.3"
}
4 changes: 4 additions & 0 deletions core/data/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id("kotlin")
id("com.google.devtools.ksp")
id("org.jlleitschuh.gradle.ktlint") version "11.5.0"
}

Expand All @@ -18,4 +19,7 @@ dependencies {
testImplementation("com.google.truth:truth:1.1.4")
testImplementation("com.squareup.okhttp3:mockwebserver:4.11.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1")

implementation("com.google.dagger:hilt-core:2.47")
ksp("com.google.dagger:hilt-android-compiler:2.47")
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package camp.nextstep.edu.github.data.network

import camp.nextstep.edu.github.domain.GithubRepository
import camp.nextstep.edu.github.domain.network.GitHubDataSource
import camp.nextstep.edu.github.domain.network.GithubDataSource
import javax.inject.Inject

internal class GithubRepositoriesDataSource(
private val gitHubService: GitHubService
) : GitHubDataSource {
internal class GithubRepositoriesDataSource @Inject constructor(
private val gitHubService: GithubService
) : GithubDataSource {
override suspend fun fetchRepositories(): List<GithubRepository> {
return gitHubService.getRepositories()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package camp.nextstep.edu.github.data.network
import camp.nextstep.edu.github.domain.GithubRepository
import retrofit2.http.GET

interface GitHubService {
@GET("/repositories")
interface GithubService {
@GET("repositories")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/를 제거하신 이유가 있을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retrofit의 introduction에 보니 앞에 /가 붙지 않아 제거했습니다.

suspend fun getRepositories(): List<GithubRepository>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package camp.nextstep.edu.github.data.network.di

import camp.nextstep.edu.github.data.network.GithubRepositoriesDataSource
import camp.nextstep.edu.github.domain.network.GithubDataSource
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@InstallIn(SingletonComponent::class)
@Module
abstract class DataModule {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DataModule로 되어있지만 실제로는 네트워크 의존성 추가 작업까지 하고 있습니다. 나눠보면 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정해보았습니다. 맞게 적용된지 잘 모르겠네요.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

잘 적용해주셨습니다 😊


@Binds
internal abstract fun bindDataSource(dataSource: GithubRepositoriesDataSource): GithubDataSource
}
Loading