Hun's Blog

Android Dagger-Hilt 적용기 (3) - Network 본문

Android

Android Dagger-Hilt 적용기 (3) - Network

jhk-im 2020. 11. 7. 03:51

아래의 오픈소스를 통해서 분석하고 학습하여 개인프로젝트에 Dagger-Hilt적용

github.com/android/architecture-samples/tree/dev-hilt

 

android/architecture-samples

A collection of samples to discuss and showcase different architectural tools and patterns for Android apps. - android/architecture-samples

github.com

github.com/skydoves/Pokedex

 

skydoves/Pokedex

🗡️ Android Pokedex using Hilt, Motion, Coroutines, Flow, Jetpack (Room, ViewModel, LiveData) based on MVVM architecture. - skydoves/Pokedex

github.com

OkHttp - Interceptor

class HttpRequestInterceptor : Interceptor {
  override fun intercept(chain: Interceptor.Chain): Response {
    val originalRequest = chain.request()
    val request = originalRequest.newBuilder().url(originalRequest.url).build()
    Timber.d(request.toString())
    return chain.proceed(request)
  }
}

Interceptor

okhttp3의 클래스이다. 

HTTP 호출을 모니터하고 재작성하며 재요청 한다. 

chain.proceed(request)는 모든 HTTP 작업을 수행하는 메서드이며 request에 대한 response를 생산한다.

여러가지 형태의 interceptor를 작성할 수 있고 서로 묶어 순서를 지정하여 구현할 수 있다. 

해당 예제에선 한가지의 interceptor만 구형되어 있다.

 

Timber는 log를 기반으로 쓰여진 라이브러리이며 간단하게 설명하면 debug에만 log를 사용하고 release에선 사용하고 싶지 않을 때 사용하는 라이브러리이다. 

 

해당 interceptor는 아래의 dagger module에서 호출된다.  

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
	...
}

 

해당 모듈 내부에 구현되어 제공되는 메서드들은 일정한 방향으로 주입되고 있는데 전체적으로 흐름을 살펴보면 조금 더 이해하기 쉽다. 

 

1. UI Controller (ex-> activity)에서 viewModel 생성

2. viewModel를 생성하기 위해 주입되어야 하는 Repository 생성 

3. Repoitory를 생성하기 위해 주입되어야 하는 NetworkClient와 localDao 생성 

 

a를 생성하기위해 주입되어야 하는 b를 생성한다는 것은 다음과같다. 

class MainRepository @Inject constructor(
  private val pokedexClient: PokedexClient,
  private val pokemonDao: PokemonDao
) : Repository {
...
}

참고하는 오픈소스의 Repository이다. 

Repository @Injection constructor(networkClient, localDao) 

-> Repository가 호출되어 생성될 때 networkClient, localDao 가 주입되어야 한다는 뜻이다. 

여기서 주입되어야 하는 것을 구현하는 것이 바로 Dagger이다. 

지난번 local database 에서 구현했던 dagger provides 메pokeapi.co/api/v2/")서드이다. 

  @Provides
  @Singleton
  fun providePokemonDao(appDatabase: AppDatabase): PokemonDao {
    return appDatabase.pokemonDao()
  }

특정 localDao가 생성될 때 dagger @Module에 등록된 porovideLocalDao() 메서드를 통해 localDao를 생성하고 반환하게 된다.

 

일반적인 흐름이라면 다음과 같을 것이다. 

UI controller 에서 viewModel을 생성할 때 Repository를 생성하고 입력해야하며 Repository생성하기 위해 networkClient와 localDao도 생성하고 입력해야 할 것이다. 

Dagger를 통해 이러한 흐름을 `일단 호출 -> 생성되어야할 di 객체 감지 -> dagger module에서 생성 반환` 이라는 흐름으로 변경되면서 첫 시작점인 UI controller에서의 로직이 상당부분 줄어들게 된 것이다.  

 

이것만으로 장점이라 할 수는 없는게 dagger가 없는 mvvm 패턴에서 repository injection 로직을 따로 구현하고 호출해서 사용하게 되면 마찬가지로 UI controller에서의 로직이 줄어든다. 확실한 장점 파악은 아직 무리임으로 일단은 흐름을 파악하는 것 에서 만족하고 다음으로 넘어가보도록 하겠다.  

 

Retrofit2

- Http client 라이브러리이다. 

- 네트워크로 전달 된 데이터를 애플리케이션에서 필요한 형태의 객체로 받을 수 있도록 해준다. 

- OkHttp3에 의존적이다. 

- OkHttp + Rerofit 조합의 통신은 안드로이드에서 공식적으로 권장하는 통신 방법이다. 

- Model, ServiceInterface, Builder 클래스가 필요하다.  

 

Repsitory 생성시 NetworkClient가 dagger를 통해 어떤 흐름으로 주입되어 생성되는지 알 수 있다. 

0. Repository를 생성하기 위해 주입되어야하는 NetworkClient생성 

1. NetworkClient를 생성하기 위해 주입되어야 하는 RetrofitService생성 

2. RetrofitService를 생성하기 위해 주입되어야 하는 Retrofit객체에 RetrofitService를 입력하여 create

3. Retrofit객체를 생성하기 위해 주입되어야 하는 OkHttpClient를 Retrofit client로 등록하여 build 

4. OkHttpClient를 생성할 때 추가해야할 Interceptor 생성 클래스를 호출 (위에 작성한 interceptor 생성 클래스)

5. interceptor 생성 클래스에서 intercept메서드를 통해 Interceptor 생성 후 반환 

 

이제 개인 프로젝트에도 다음을 적용해보도록 하겠다. 

 @Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

  @Provides
  @Singleton
  fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
    return Retrofit.Builder()
      .client(okHttpClient)
      .baseUrl("https://app.sportdataapi.com/api/v1/soccer/leagues")
      .build()
  }
 
  @Provides
  @Singleton
  fun provideRetrofitService(retrofit: Retrofit): RetrofitService {
    return retrofit.create(RetrofitService::class.java)
  }

  @Provides
  @Singleton
  fun provideRetrofitClient(retrofitService: RetrofitService): RetrofitClient {
    return RetrofitClient(retrofitService)
  }
 }

`RetrofitClient를 생성 -> RetrofitService주입 -> RerofitService 생성 -> Retrofit 주입 -> Retrofit생성 -> OkHttpClient 주입 -> OkHttpClient생성`

 

API 서버로부터 네트워크 데이터를 가져와 애플리케이션에 맞게 데이터를 가공해줄 RetrofitClient를 생성하는 과정이라고 생각하면 될 것 같다. 

 

RetrofitClient 클래스와 RetrofitService 인터페이스를 자세히 구현해보자. 

 

Model은 다음과 같이 setting 하였다. 

@Entity(tableName = "movies")
@Parcelize
data class Movie(
    @PrimaryKey val id: Int,
    @ColumnInfo(name = "title") val title: String,
    @ColumnInfo(name = "poster_path") val poster_path: String,
    @ColumnInfo(name = "popularity")val popularity: Float,
    @ColumnInfo(name = "vote_count") val vote_count: Int,
    @ColumnInfo(name = "vote_average") val vote_average: Float,
    @ColumnInfo(name = "page") val page: Int?
) : Parcelable {

    fun getVoteCountString(): String = "$vote_count"
    fun getVoteAverageString(): String ="$vote_average"
    fun getPopularityString(): String = "$popularity"
    fun getImageUrl(): String = "https://image.tmdb.org/t/p/w200$poster_path"
}

 

API 는 아래의 movie opensource API를 사용한다. 

developers.themoviedb.org/3/movies/get-movie-lists

 

API Docs

Hosted API documentation for every OAS (Swagger) and RAML spec out there. Powered by Stoplight.io. Document, mock, test, and more, with the StopLight API Designer.

developers.themoviedb.org

interface RetrofitService {

    @GET("discover/movie")
    suspend fun getMovies(@QueryMap par: Map<String, String>): MovieListResponse
}

retofit service를 api 형식에 맞게 구현하였다. 

코루틴에서 구현되기 때문에 suspend fun으로 설정한다. 

json 데이터를 넘겨받기 때문에 @QueryMap을 parameter로 지정하고 MovieListReponse로 반환한다. 

MovieListResponse에는 <parameter , movieList> 가 설정된다. 다음과같다. 

class MovieListResponse(
    var page: Int,
    val results: List<Movie>
)

 

Test를 해보기위해 MainActivity에 임시로직을 작성한다. 

class MainActivity : AppCompatActivity() {

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

        val retrofit = Retrofit.Builder()
            .baseUrl("https://api.themoviedb.org/3/")
            .addConverterFactory(MoshiConverterFactory.create())
            .addCallAdapterFactory(CoroutineCallAdapterFactory())
            .build()

        val retrofitService = retrofit.create(RetrofitService::class.java)

        val textView = findViewById<TextView>(R.id.test_text)
        val param = mapOf( // (1) GET 요청용 변수를 mapOf()를 사용해 지정
            "page" to "1",
            "api_key" to API-KEY,
            "sort_by" to "popularity.desc",
            "language" to "en"
        )

        GlobalScope.launch(Dispatchers.Main) { // (1) 코루틴의 launch 빌더 사용
            try {
                for (movie in retrofitService.getMovies(param).results) {
                    Log.e("movie","${movie.title}")
                }

            } catch (e: Throwable) { // (3)
                Log.e("","${e.message}")
            }
        }
    }
}

 

아직 dagger module은 사용하지 않기 때문에 rerofit 을 직접 빌드하여 테스트하였다. 

Retrofit bulid시 아래의 ConverterFactory와 CallAdapterFactory를 추가한다. 

.addConverterFactory(MoshiConverterFactory.create())

-> 넘겨받은 json을 프로젝트의 모델에 맞게 변형하기위해 컨버터를 설정한다.

  -> Gson, Moshi 등 여러가지 컨버터가 있는데 참고하는 예제에선 Moshi를 사용한다.

     -> Moshi가 아닌 다른것을 설정하면 오류가 나는데 이부분은 한번 더 자세히 알아봐야 할 것 같다. 
.addCallAdapterFactory(CoroutineCallAdapterFactory())

-> 코루틴 내부에서 Retrofit service를 실행할 때 해당 어댑터가 설정되어있지 않으면 계속 오류가 발생하였다. 

  -> 이부분도 역시 자세히 한번 더 알아봐야 할 것 같다. 

 

parma = map of() 는 HTTP GET의 파라미터에 입력해야하는 값이다. 

retrofitService.getMovies(param).results 실행하면 api 서버로부터 최신 영화 리스트 1page 분량을 받아오게 된다. 

앱을 실행하면 아래와 같이 movie title을 로그에 작성한다. 

 

 

local 과 network가 설정되었으니 이제 ui controller - view model - repository 셋팅해보도록 하겠다. 

 

 

-참고자료=

 

acaroom.net/ko/blog/youngdeok/%EC%97%B0%EC%9E%AC-%EC%BD%94%ED%8B%80%EB%A6%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-02-7%EB%8B%A8%EA%B3%84-%EC%BD%94%EB%A3%A8%ED%8B%B4%EC%9D%98-%EC%82%AC%EC%9A%A9#%EC%BD%94%EB%A3%A8%ED%8B%B4%EC%9D%84%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94launch()%EB%A1%9C%EB%B3%80%EA%B2%BD

 

[연재] 코틀린 프로젝트 - 02 - 7단계: 코루틴의 사용

이번에는 기존에 만들어진 RxJava 대신에 코루틴을 사용해 바꿔 봅시다. 코루틴을 사용하면 RxJava보다 가볍고 비동기 처리루틴을 사용하기가 더 쉽습니다. RxJava에서 사용되었던 구독자와 생산자

acaroom.net

 

salix97.tistory.com/204

 

레트로핏 (Retrofit) 이란? (Kotlin 으로 레트로핏 사용)

1. retrofit 의 뜻 안드로이드에서 retrofit 이 무엇인지 알아보기 전에, retrofit 이라는 단어의 사전적인 의미부터 알아보자. (1) 기존에 사용할 수 없었던 필요하다고 간주되는 새 부품이나 개조된 장

salix97.tistory.com