Hun's Blog

Android Dagger-Hilt 적용기 (5) - Repositoy 본문

Android

Android Dagger-Hilt 적용기 (5) - Repositoy

jhk-im 2020. 11. 11. 04:16

아래의 오픈소스를 통해서 분석하고 학습하여 개인프로젝트에 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

Repository

  • Repository는 여러 데이터 소스에 대한 액세스를 추상화한다.
  • 아키텍처 구성요소 라이브러리에 속하지 않지만 코드 분리 및 아키텍처를 위한 권장 모범 사례다.
  • 애플리케이션의 나머지 부분에 대한 데이터 액세스를 위한 API를 제공한다.
  • Qeury를 관리하고 여러 백엔드를 사용할 수 있도록 허용한다.
  • Network와 Local 데이터베이스 사이에서 캐시된 결과를 어떻게 사용할지를 결정하기위한 로직을 구현한다.

`코드 분리 및 아키텍처를 위한 권장` 사항으로 참고하는 두가지의 오픈소스에서 모두 구현한다. 

간단하게 표현하면 network와 local 사이에서 데이터의 흐름을 관리한다고 할수있다. 

 

참고하는 오픈소스의 로직을  정확히 필요한 내용만 살펴보도록 하겠다.

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

  @WorkerThread
  suspend fun fetchPokemonList(
    page: Int,
    onSuccess: () -> Unit,
    onError: (String) -> Unit
  ) = flow {
    ...
	emmit()
    ...
  }.flowOn(Dispatchers.IO)
}

 

Repository 또한 DI가 구현되어있으며 repository가 생성 될때 이전에 만들었던 networkClient와 localDao 가 주입된다. 각자의 module로가서 사용할 수 있는 상태가 되도록 생성되어 주입된다. 

 

@WorkThread

androidx의 annotaion이다. 

메서드가 특정 유형의 스레드로부터 호출되는지 확인한다. 

예를들어 ui 변경작업의 경우 스레드에서 에러가 발생하는데 이 경우 사전에 알려주는 역할을 한다. 

 

flow { ... }.flowOn(Dispatchers.IO)

특정 연산을 수행한 후 한개의 값을 반환하는 중단 메서드를 정의하고 이를 비동기로 수행할 수 있다. 하지만 연산 후 두개 이상의 값을 반환하는 중단메서드는 Kotlin Flow를 이용하여 구현해야한다. 

suspend fun 키워드로 선언된 fetchPokemonList 는 중단 메서드는 코루틴 스코프에서 호출하여 호출 스레드의 정지없이 실행할 수 있다. 해당 메서드는 List를 반환해야하고 이것은 모든 연산을 수행한 후 모든 값을 반환해야 함을 의미한다.

Kotlin Flow을 사용하면 값을 순차적으로 방출하는 데이터 스트림을 처리할 수 있다.

  • Flow 타입을 생성은flow {} 빌더를 이용한다.
  • flow { ... } 블록 안의 코드는 중단 가능하다.
  • 결과 값들은 flow 에서 emit() 함수를 이용하여 방출된다. 

 

이제 주요한 기능들은 파악이 된 것 같으니 viewModel과 repository에 로직을 구현하여 간단하게 테스트해보도록 하겠다. 

테스트는 간단하다. repository에서 local database를 체크하고 값이 비어져있으면 networkClient를 통해 api 서버에서 영화 리스트를 가져온다. 

 

MainActivity 진입부터 흐름을 다시 정리해보고 이전에 추가하지 못한 부분을 정리해보자. 

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @VisibleForTesting val mainViewModel: MainViewModel by viewModels()
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        binding.apply {
            lifecycleOwner = this@MainActivity
            viewModel = mainViewModel
        }
    }
}

 

1. 우선 databinding을 구현한 ActivityMainBinding을 MainActivity와 연결한다.

   -> 공식문서에서 소개하는 기본적인 연결 로직을 사용하였다. 

 

2. databinding이 생성되면 viewModel이 생성되고 MainRepository가 주입된다. 

 

3. * 여기서 MainRepository의 주입과정을 Dagger-hilt module인 RepositoryModule에 아래와 같이 구현한다. 

@Module
@InstallIn(ActivityRetainedComponent::class)
object RepositoryModule {

    @Provides
    @ActivityRetainedScoped
    fun provideMainRepository(
        retrofitClient: RetrofitClient,
        movieDao: MovieDao
    ): MainRepository {
        return MainRepository(retrofitClient, movieDao)
    }

}

4. MainRepository가 생성되면 RetrofitClient와 MovieDao가 주입된다. 

 

5. * RetrofitClient에 지난번 테스트했던 로직을 참고하여 다음과 같이 로직을 작성한다. 

class RetrofitClient @Inject constructor(
    private val retrofitService: RetrofitService
) {

    suspend fun fetchMovies(page: Int): MovieListResponse {
        val param = mapOf( // (1) GET 요청용 변수를 mapOf()를 사용해 지정
            "page" to page.toString(),
            "api_key" to "e7b63af5659f57f6415baadfc9a3c6c5",
            "sort_by" to "popularity.desc",
            "language" to "en"
        )
        return retrofitService.getMovies(param)
    }

}

 

6. * MovieDao의 주입 과정을 LocalMudule에 다음과같이 구현한다. 

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

    @Provides
    @Singleton
    fun provideMoshi(): Moshi {
        return Moshi.Builder().build()
    }

    @Provides
    @Singleton
    fun provideAppDatabase(
        application: Application
    ): AppDatabase {
        return Room
            .databaseBuilder(application, AppDatabase::class.java, "Movies.db")
            .fallbackToDestructiveMigration()
            .allowMainThreadQueries()
            .build()
    }

    @Provides
    @Singleton
    fun provideMovieDao(appDatabase: AppDatabase): MovieDao {
        return appDatabase.movieDao()
    }
}

 

7. *MainRepository 내부에 오픈소스를 참고하여 flow {}.flowOn(Dispatchers.IO) 내부에 다음과같은 로직을 추가한다. 

class MainRepository @Inject constructor(
    private val retrofitClient: RetrofitClient,
    private val movieDao: MovieDao
):Repository {

    @WorkerThread
    suspend fun getMovies(
        page: Int
    ) = flow {
        val movies = movieDao.getMovies(page)
        if (movies.isEmpty()) {
            val newMovies = ArrayList<Movie>()
            for (movie in retrofitClient.fetchMovies(page).results) {
                movie.page = page
                newMovies.add(movie)
            }
            movieDao.saveMovies(newMovies).apply {  }
            emit(newMovies)
        } else {
            emit(movies)
        }
    }.flowOn(Dispatchers.IO)
}

local에서 데이터를 검색하고 리스트가 비어있으면 네트워크로부터 데이터를 받아와 local database에 저장한다. 

 

8. *ViewModel에서 MainRepostory에서 가져온 데이터를 Log를 통해 확인하는 로직을 추가한다. 

class MainViewModel @ViewModelInject constructor(
    private val mainRepository: MainRepository,
    @Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    init {

        viewModelScope.launch(Dispatchers.IO) { // (1) 코루틴의 launch 빌더 사용
            try {
                mainRepository.getMovies(1).collect {
                    for (movie in it) {
                        Log.e("title","${movie.title}")
                    }
                }
            } catch (e: Throwable) { // (3)
                Log.e("","${e.message}")
            }
        }
    }

    @MainThread
    fun getMovies(page: Int) {
    }
}

 

9. 최종 테스트 결과 

local database 저장 확인

 

다음 글에선 이렇게 받아온 데이터를 MainActivity에서 리스트로 보여주기위한 로직을 구현해보도록 하겠다. 

 

 

참고자료

medium.com/@myungpyo/%EC%BD%94%EB%A3%A8%ED%8B%B4-%EA%B3%B5%EC%8B%9D-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%9E%90%EC%84%B8%ED%9E%88-%EC%9D%BD%EA%B8%B0-part-9-a-d0082d9f3b89

 

코루틴 공식 가이드 자세히 읽기 — Part 9-A

Asynchronous Flow

medium.com

blog.mindorks.com/what-is-flow-in-kotlin-and-how-to-use-it-in-android-project

 

What is Flow in Kotlin and How to use it in Android Project?

What is Flow in Kotlin and How to use it in Android Project? In this blog we will learn what is the need of Flow in Kotlin and its explaination.

blog.mindorks.com