Skip to content
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

Step2 - GitHub(HTTP) #108

Open
wants to merge 11 commits into
base: oyj7677
Choose a base branch
from
Open
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# android-github

## 1 단계 - GitHub(모듈 분리)
### 프로그래밍 요구 사항
- 순수 코틀린 모듈인 domain 모듈을 만든다.
- 순수 코틀린 모듈인 data 모듈을 만든다.
- data 모듈은 domain 모듈에 의존해야 한다.
- app 모듈은 domain 모듈에 의존해야 한다.

## 2단계 - GitHub(HTTP)
###기능 요구사항
- GitHub Repository 목록을 가져올 수 있어야 한다.
- GET, https://api.github.com/repositories
- full_name, description 필드만

###프로그래밍 요구사항
- Repository 목록을 가져오는 기능은 data 모듈에 구현되어야 한다.
- data 모듈의 구현체는 모두 internal class로 선언한다.
- HTTP 요청을 통해 가져오는 구현체에 대한 테스트 코드를 작성한다.
- OkHttp MockWebServer 이용
23 changes: 15 additions & 8 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
@file:Suppress("UnstableApiUsage")

plugins {
id("com.android.application")
id("kotlin-android")
Expand All @@ -10,7 +8,7 @@ android {
compileSdk = Version.compileSdk

defaultConfig {
applicationId = "edu.nextstep.camp.calculator"
applicationId = "camp.nextstep.edu.github"
minSdk = Version.minSdk
targetSdk = Version.targetSdk
versionCode = 1
Expand Down Expand Up @@ -42,17 +40,26 @@ android {
}

dependencies {
api(project(":github-domain"))
implementation(project(":github-data"))
implementation(project(":github-domain"))

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.squareup.retrofit2:retrofit:2.9.0")
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
implementation ("com.google.code.gson:gson:2.10.1")
implementation("androidx.room:room-runtime:2.5.2")
annotationProcessor("androidx.room:room-compiler:2.5.2")

testImplementation("junit:junit:4.13.2")
testImplementation("com.google.truth:truth:1.1.3")
testImplementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
implementation("com.squareup.okhttp3:mockwebserver:4.11.0")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}

2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<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"
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/camp/nextstep/edu/github/GitHubViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package camp.nextstep.edu.github

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.github_domain.GitHubRepository
import kotlinx.coroutines.launch

class GitHubViewModel(private val gitHubRepository: GitHubRepository) : ViewModel() {

fun insertGitHubRepository() {
gitHubRepository.insertGitHubRepository()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package camp.nextstep.edu.github

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.github_data.room.GithubRepoDao
import com.example.github_data.room.Injector

class GithubViewModelFactory(private val githubRepoDao: GithubRepoDao) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(GitHubViewModel::class.java)) {
return GitHubViewModel(Injector.providesGithubRepoRepository(githubRepoDao)) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
34 changes: 33 additions & 1 deletion app/src/main/java/camp/nextstep/edu/github/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,42 @@ package camp.nextstep.edu.github

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.lifecycle.ViewModelProvider
import camp.nextstep.edu.github.databinding.ActivityMainBinding
import com.example.github_data.room.GithubRepoDao
import com.example.github_data.room.GithubRepoDatabase
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MainActivity : AppCompatActivity() {

Choose a reason for hiding this comment

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

UI의 경우는 Step3의 미션입니다 ㅠ,ㅠ! 관련 변경사항은 step3진행하면서 적용해주시면 좋을거같아요!


private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: GitHubViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

initBinding()
initViewModel()

setContentView(binding.root)
}

private fun initViewModel() {
val githubRepoDao = GithubRepoDatabase.getInstance(this)!!.GithubRepoDao()

Choose a reason for hiding this comment

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

!! 는 null일 경우, 에러가 발생하니 사용을 지양하는편이 좋아요
유연한 null처리방법을 활용해보아요

val githubViewModelFactory = GithubViewModelFactory(githubRepoDao)

Choose a reason for hiding this comment

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

dao라는 데이터 레이어의 구현체를 app모듈에서 알아야할까요? 추가로 고민하면 좋을거같아요!

viewModel = ViewModelProvider(this, githubViewModelFactory)[GitHubViewModel::class.java]
binding.viewModel = viewModel
}

private fun initBinding() {
binding = ActivityMainBinding.inflate(layoutInflater)
binding.lifecycleOwner = this
}
}
}
42 changes: 24 additions & 18 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
>
<layout>
<data>
<variable
name="viewModel"
type="camp.nextstep.edu.github.GitHubViewModel" />
</data>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

</androidx.constraintlayout.widget.ConstraintLayout>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="github/repositories 저장"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:onClick="@{() -> viewModel.insertGitHubRepository()}"/>

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
56 changes: 56 additions & 0 deletions app/src/test/java/camp/nextstep/edu/github/GitHubViewModelTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package camp.nextstep.edu.github

import com.example.github_data.GitHubApi
import com.example.github_data.room.GithubRepoEntity
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.Before
import org.junit.Test
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import java.io.File

class GitHubViewModelTest {
val server = MockWebServer()

Choose a reason for hiding this comment

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

viewModel 테스트도 좋지만,

Repository 목록을 가져오는 기능은 data 모듈에 구현되어야 한다.

데이터모듈에서 테스트코드를 작성해보면 어떨까요?



private lateinit var service: GitHubApi

Choose a reason for hiding this comment

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

api를 테스트하기 보단 GitHubRepositoryImpl를 테스트하는게 의미있지않을까요?


@Before
fun init() {
val baseUrl = server.url("")

service = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(baseUrl)
.build()
.create()
}

@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `MockWebServer를 활용하여 getGitHubInfo() 요청을 검증한다`() = runTest(UnconfinedTestDispatcher()) {
// given : MockResponse을 활용해 서버 응답을 세팅해둔다.
val response = MockResponse()
.setBody(File("src/test/resources/githubRepo.json").readText())

Choose a reason for hiding this comment

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

정상케이스의 테스트도 좋지만,
빈리스트,
에러 등의 케이스도 고민해보아요!

.setResponseCode(200)

server.enqueue(response)

// when : https://api.github.com/repositories에 HTTP통신 요청을 한다.
val actual = service.getGitHubInfo().body()

// then : 응답 받은 값과 비교한다.
val expected = listOf(GithubRepoEntity("1", "mojombo/grit", "**Grit is no longer maintained. Check out libgit2/rugged.** Grit gives you object oriented read/write access to Git repositories via Ruby."),
GithubRepoEntity("26", "wycats/merb-core", "Merb Core: All you need. None you don't."),
GithubRepoEntity("27", "rubinius/rubinius", "The Rubinius Language Platform"),
GithubRepoEntity("28", "mojombo/god", "Ruby process monitor"),)

assertThat(actual).isEqualTo(expected)
}
}
Loading