관리 메뉴

막내의 막무가내 프로그래밍 & 일상

[안드로이드] Android Hilt 적용해본 코드 기록!! 본문

안드로이드/코틀린 & 아키텍처 & Recent

[안드로이드] Android Hilt 적용해본 코드 기록!!

막무가내막내 2021. 5. 24. 20:18
728x90

 

 

 

 

 

 

https://youngest-programming.tistory.com/545

 

[안드로이드] 구글 공식 프로젝트 Sunflower 스터디 (4) Hilt Dependency Injection

[2021-04-29 업데이트] [출처 및 참고] github.com/android/sunflower android/sunflower A gardening app illustrating Android development best practices with Android Jetpack. - android/sunflower github...

youngest-programming.tistory.com

이전에 위 포스팅에서 구글 프로젝트를 통해 Hilt에 대해 정리한 적이 있는데 이번 공모전 개발에서 직접 사용해보았습니다. (시간이 없어 프로젝트는 구성은 개판일지라도 하나라도 새로운 기술을 써보기 위해..ㅎㅎ) 아직 초급단계지만 사용해보며 확실히 Dagger2 를 처음 접했을때보단 훨씬 간편하고 쉬워졌다고 느낄 수 있었습니다...!

 

 

 

 

적용해본 코드를 기록하여 나중에 참고할 수 있게 간단하게 포스팅 해보려고 합니다. 

[프로젝트 레포]

https://github.com/OnzeGgaaziFlow

 

OnzeGgaaziFlow

2021 정부혁신제안 끝장개발대회 출품작 '환경메이트'. OnzeGgaaziFlow has 2 repositories available. Follow their code on GitHub.

github.com

 

 

 

 

먼저 아키텍처를 살펴보면 MVVM 구조로 짯고 Model 부분을 API 통신만 연결할거므로 Local, Remote를 나누지않고 Repository만 만들어 사용했습니다. 

 

기록용이고 이전 포스팅에 설명을 달아놨으므로 설명은 생략하도록 하겠습니다. :)

 

[Application]

@HiltAndroidApp 설정

@HiltAndroidApp
class MyApplication : Application() {
}

 

[Activity]

@AndroidEntryPoint  설정

@AndroidEntryPoint
class MainActivity : BaseActivity<ActivityMainBinding>(R.layout.activity_main) {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initNavigation()
    }

 

[Fragment]

@AndroidEntryPoint  위 MainActivity에 종속되는 Fragment 입니다. 반드시 이것도 어노테이션 지정 필요

@AndroidEntryPoint
class ChartFragment :
    BaseFragment<FragmentChartBinding>(R.layout.fragment_chart) {

    private val viewModel: ChartViewModel by viewModels()
    override fun init() {
        binding.vm = viewModel
        initView()
        initAdapter()
        initViewModelCallback()
    }

 

 

[Adapter]

처음에 어댑터도 밑에 링크 나오는 예제처럼

https://stackoverflow.com/questions/63697582/how-to-inject-adapter-with-hilt

 

How to inject adapter with hilt

I have a complete recyclerView example, now I want to use hilt inject the ItemListAdapter of this example to my ItemListFragment. But it seems something that can not be done if I still want the way...

stackoverflow.com

 @Inject
lateinit var chartMissionAdapter: ChartMissionAdapter
class ChartMissionAdapter @Inject constructor(private val onItemClick: (IndustryEnergy) -> Unit) 
@Module
@InstallIn(SingletonComponent::class)
class AdapterModule {

    @Provides
    @Singleton
    fun provideChatMissionAdapter(): ChartMissionAdapter = ChartMissionAdapter()

    private var onItemClickListener: ((IndustryEnergy) -> Unit)? = null
    fun setOnItemClickListener(listener: (IndustryEnergy) -> Unit) {
        onItemClickListener = listener
    }
}

이런식으로 주입시켜 사용하려 했으나 클릭리스너 부분을 상당 부분 바꿔야하고 애매해서 사용하지 않게되었습니다. 

 

 

[ViewModel]

@HiltViewModel + @Inject constructor로 주입

@HiltViewModel
class ChartViewModel @Inject constructor(private val repository: ChartRepository) :
    BaseViewModel() {

 

[Repository]

싱글톤 모듈을 만들고 생성자 파라미터를 주입받습니다.

class ChartRepositoryImpl @Inject constructor(private val mainApiInterface: MainApiInterface) :
    ChartRepository {
@InstallIn(SingletonComponent::class)
@Module
class DatabaseModule {

    @Singleton
    @Provides
    fun provideUserInfoRepository(apiInterface: ApiInterface): UserInfoRepository {
        return UserInfoRepositoryImpl(apiInterface)
    }

    @Singleton
    @Provides
    fun provideSignInRepository(apiInterface: ApiInterface): SignInRepository {
        return SignInRepositoryImpl(apiInterface)
    }

    @Singleton
    @Provides
    fun provideChartRepository(mainApiInterface: MainApiInterface): ChartRepository {
        return ChartRepositoryImpl(mainApiInterface)
    }
}

 

 

[ApiModule]

서버와 통신할 Retrofit2 관련 코드들과 모듈들입니다. Header에 인증토큰을 담아야하는 경우와 아닌 경우가 있어 두 가지를 만들었고 @Singleton으로 적용하여 재생성하는 비효율을 줄였습니다.

추가로 @Field 대신 @FieldMap을 사용해도 됩니다.

@InstallIn(SingletonComponent::class)
@Module
class ApiModule {

    @Singleton
    @Provides
    fun provideApiService(): ApiInterface {
        return ApiInterface.create()
    }

    @Singleton
    @Provides
    fun provideMainApiService(): MainApiInterface {
        return MainApiInterface.create()
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mtjin.envmate.api

import com.mtjin.envmate.data.model.response.LoginRes
import com.mtjin.envmate.data.model.response.SignUpRes
import io.reactivex.Single
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST


interface ApiInterface {

    @FormUrlEncoded
    @POST("accounts/signup-req")
    fun insertUserInfo(
        @Field("business_name") businessName: String,
        @Field("business_number")
        businessNumber: String,
        @Field("officer_email")
        officerEmail: String,
        @Field("officer_name")
        officerName: String,
        @Field("officer_phone")
        officerPhone: String,
        @Field("officer_position")
        officerPosition: String,
        @Field("password")
        password: String,
        @Field("industry")
        industry: String,
        @Field("location_name")
        locationName: String
    ): Single<SignUpRes>

    @FormUrlEncoded
    @POST("accounts/signup-accept")
    fun requestSignUpAccept(
        @Field("officer_email") officeEmail: String
    ): Single<SignUpRes>

    @FormUrlEncoded
    @POST("accounts/login/")
    fun requestLogin(
        @Field("email") email: String,
        @Field("password") password: String
    ): Single<LoginRes>


    companion object {
        private const val BASE_URL =
            "http://6e3ac85e023a.ngrok.io"

        fun create(): ApiInterface {
            val logger = HttpLoggingInterceptor().apply {
                level =
                    HttpLoggingInterceptor.Level.BASIC
            }
            val client = OkHttpClient.Builder()
                .addInterceptor(logger)
                .build()

            return Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(client)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(ApiInterface::class.java)
        }
    }
}
package com.mtjin.envmate.api

import android.util.Log
import com.mtjin.envmate.data.model.response.EnvRes
import com.mtjin.envmate.data.model.response.IndustryEnergyRes
import com.mtjin.envmate.utils.UserInfo
import io.reactivex.Single
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query

interface MainApiInterface {

    @GET("datas/compare/region")
    fun requestCompareRegion(): Single<EnvRes>

    @GET("datas/compare/same-region")
    fun requestCompareSameRegion(
        @Query("usage") usage: Int = 80000
    ): Single<EnvRes>

    @GET("datas/compare/industry-all")
    fun requestCompareIndustryAllEnv(): Single<EnvRes>

    @GET("datas/compare/industry-sameall")
    fun requestCompareIndustrySameAll(@Query("usage") usage: Int): Single<EnvRes>

    @GET("datas/detail/industry-energy")
    fun requestDetailIndustryEnergy(
        @Query("gas") gas: Int,
        @Query("other") other: Int,
        @Query("oil") oil: Int,
        @Query("coal") coal: Int,
        @Query("thermal") thermal: Int,
        @Query("electric") electric: Int
    ): Single<IndustryEnergyRes>

    companion object {
        private const val BASE_URL =
            "http://6e3ac85e023a.ngrok.io"

        fun create(): MainApiInterface {
            val logger = HttpLoggingInterceptor().apply {
                level =
                    HttpLoggingInterceptor.Level.BASIC
            }
            val interceptor = Interceptor { chain ->
                with(chain) {
                    val newRequest = request().newBuilder()
                        .addHeader("Authorization", "Token " + UserInfo.headerKey)
                        .build()
                    proceed(newRequest)
                }
            }
            Log.d("AAAAA", "Token " + UserInfo.headerKey)
            val client = OkHttpClient.Builder()
                .addInterceptor(logger)
                .addInterceptor(interceptor)
                .build()

            return Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(client)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(MainApiInterface::class.java)
        }
    }
}

 

 

 


이전 프로젝트에서는 외부 API까지만 다루고 LocalDataSource부분은 다루지 않았는데요.

다른 프로젝트에서 이부분을 다뤄 Local 부분을 추가로 작성합니다.

https://github.com/mtjin/mtjin-deeplink-tester-android

 

GitHub - mtjin/mtjin-deeplink-tester-android

Contribute to mtjin/mtjin-deeplink-tester-android development by creating an account on GitHub.

github.com

 

[LocalDataSourceModule]

Room 데이터베이스 생성을 의존성 주입하는 것과 LocalDataSource 클래스에 의존성주입을 하는 코드입니다.

  
package com.mtjin.mtjindlt.di

import android.content.Context
import androidx.room.Room
import com.mtjin.mtjindlt.data.database.MtjinRoomDatabase
import com.mtjin.mtjindlt.data.main.source.local.MainDao
import com.mtjin.mtjindlt.data.main.source.local.MainLocalDataSource
import com.mtjin.mtjindlt.data.main.source.local.MainLocalDataSourceImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@InstallIn(SingletonComponent::class)
@Module
class LocalDataSourceModule {
    // Room database
    @Singleton
    @Provides
    fun provideAppDatabase(@ApplicationContext context: Context): MtjinRoomDatabase {
        return Room.databaseBuilder(
            context,
            MtjinRoomDatabase::class.java, "MtjinDLT.db"
        ).build()
    }

    // Local
    @Singleton
    @Provides
    fun provideMainLocalDataSource(mainDao: MainDao): MainLocalDataSource {
        return MainLocalDataSourceImpl(mainDao)
    }

    // Dao
    @Provides
    @Singleton
    fun provideUserDao(roomDatabase: MtjinRoomDatabase): MainDao =
        roomDatabase.mainDao()
}

 

 

 

 

[RepositoryModule]

Repository 클래스에 LocalDataSource 를 주입하는 모듈입니다.

 

package com.mtjin.mtjindlt.di

import com.mtjin.mtjindlt.data.main.source.MainRepository
import com.mtjin.mtjindlt.data.main.source.MainRepositoryImpl
import com.mtjin.mtjindlt.data.main.source.local.MainLocalDataSource
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@InstallIn(SingletonComponent::class)
@Module
class RepositoryModule {
    @Singleton
    @Provides
    fun provideMainRepository(mainLocalRepository: MainLocalDataSource): MainRepository {
        return MainRepositoryImpl(mainLocalRepository)
    }

}

 

 

 

 

이상 Hilt를 사용해본 코드를 기록한 포스팅이었습니다. 

댓글과 공감은 큰 힘이 됩니다. 감사합니다. !!

728x90
Comments