-
Notifications
You must be signed in to change notification settings - Fork 44
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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}")) } | ||
} | ||
} | ||
} | ||
} | ||
} |
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") { | ||
addMain(navController) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
private fun NavGraphBuilder.addMain(navController: NavHostController) { | ||
composable(route = "Main") { | ||
val state by viewModel.collectAsState() | ||
viewModel.collectSideEffect { | ||
navController.navigate(route = "Main") | ||
} | ||
GithubRepositoryPage(state) | ||
} | ||
} | ||
} |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사실 현재는 MaterialTheme을 그대로 사용하셔도 무방합니다. 컬러, 폰트 등 프리셋이 생기면 그때 추가해도 괜찮아보여요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 원래 프리셋 등으로 해서 적용해보는 식으로 할까 하다가 다른 속성에 대해서 적용을 해볼까 하다가 말았는데요. 현업에서 MaterialTheme보다는 커스텀한 상황을 많이 쓰겠죠? 제가 컴포즈를 잘 몰라 문의드립니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현업에서는 디자인 시스템 프리셋을 정의하는 경우가 많고, MaterialTheme을 그대로 따라가기에는 아이폰과 같은 디자인인 경우가 많아서 대부분 커스텀으로 정의하긴 합니다. 현재 미션과는 크게 관련 없으니 참고만 해주세요! |
||
MaterialTheme( | ||
content = content | ||
) | ||
} |
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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 단순 성공 / 실패 상태만 명시하기보다 이 UI 자체에 데이터의 흐름을 담아보면 어떨까요? 사용하신 MVI 라이브러리에 대한 이해도가 낮은 상태라 적절하지 않은 피드백일 수 있습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재는 state는 명시적으로 로딩 중과 성공 실패에대한 상태를 넘기고 데이터의 경우 추가적으로 넘길 수 있도록 구현하였는데요. Status 자체에 데이터를 넘기는 식으로 하는게 더 좋을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
} |
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" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. /를 제거하신 이유가 있을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DataModule로 되어있지만 실제로는 네트워크 의존성 추가 작업까지 하고 있습니다. 나눠보면 어떨까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수정해보았습니다. 맞게 적용된지 잘 모르겠네요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 잘 적용해주셨습니다 😊 |
||
|
||
@Binds | ||
internal abstract fun bindDataSource(dataSource: GithubRepositoriesDataSource): GithubDataSource | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
정보 감사합니다. 사실 해당 mission이 Main밖에 없어 이 부분에 대해서 더 보지 못하였는데요. 참고로 주신 리파지토리 보도록 하겠습니다.