개발일지

Android in A..Z - Hilt (개념) 본문

Android (안드로이드)/Hilt

Android in A..Z - Hilt (개념)

강태종 2021. 3. 18. 04:28

Hilt

기존의 Dagger2를 Android의 구조적으로 맞게 기능을 추가한 라이브러리이다. DI를 도와주며 Annotaion을 통해 보일러 플레이트 코드를 제거하고 쉽게 사용할 수 있다.


DI (Dependency Injection)

Android뿐만 아니라 프로그래밍에서 널리 사용되는 기법이고 다양한 이점이 있다.

  • 코드의 재사용성
  • 리팩토링 용이성
  • 테스트 용이성

 

클래스에서 다른 클래스를 참조하는 방법은 크게 3가지가 있습니다. (Car와 Engine을 예시로)

 

1. 클래스에서 필요한 종속 클래스를 인스턴스화하는 방법

class Car {

    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

위와 같은 방법은 Car와 Engine이 밀접하게 연결되어 있기 때문에 문제가 발생합니다.

 

1. 만약 Engine를 상속받은 GasEngine, ElectricEngine Class가 있을 때 쉽게 대체할 수 없습니다.

=> Engine이라는 한가지 유형을 사용하기 때문에 수정하면 연관된 코드를 수정해야 할 가능성이 있다.

2. 테스트를 더욱 어렵게 한다.

=> Engine을 FakeEngine으로 바꿔서 테스트 하려면 Engine과 연관된 코드를 수정해야 한다.

 

2. getter / setter를 이용한 방법 (필드 삽입, setter 삽입)

class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

3. 생성자를 통해 넘겨주는 방법 (생성자 삽입)

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

2, 3번이 DI를 이용한 프로그래밍 방법이고 1번에서 발생한 문제점을 쉽게 해결할 수 있습니다.

=> Engine의 서브 클래스들을 쉽게 대입할 수 있기 때문에 Car을 재사용할 수 있고, 쉽게 테스트할 수 있다.

 

Hilt는 2, 3번을 쉽게 도와주며 중복 코드와 보일러 코드를 최소화 할 수 있습니다.


Dependency

build.gradle(Module)

plugins {
    id 'dagger.hilt.android.plugin'
}

dependencies {
    // Hilt
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}

 

build.gradle(Project)

ext {
    hilt_version = "2.31.2-alpha"
}

dependencies {
    classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}

@HiltAndroidApp

Hilt를 사용하는 모든 Application은 @HiltAndroidApp으로 지정된 Application Class를 포함해야 합니다.

@HiltAndroidApp으로 지정된 Application Class는 Hilt의 코드 생성을 트리거합니다.

 

MainApplication.kt

Application을 상속받는 Class를 만들고, @HiltAndroidApp Annotation을 지정한다.

@HiltAndroidApp
class MainApplication : Application()

AndroidManifest.xml

application에 Name을 추가한다. (@HiltAndroidApp Annotation이 지정된 Application Class로 추가한다.)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.taetae98.hilt">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:usesCleartextTraffic="true"
        android:name=".MainApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Hilt">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

@Inject

Hilt를 사용하여 의존성을 주입할 때 사용하는 Annotation이다.

 

1. 주입할 클래스에 @Inject 생성자를 만든다.

@FragmentScoped
class SummonerInformationAdapter @Inject constructor() : BaseAdapter<SummonerInformation>(SummonerInformationItemCallback()) {
    init {
        setHasStableIds(true)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseHolder<out ViewDataBinding, SummonerInformation> {
        return SummonerInformationHolder(HolderSummonerInformationBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }

    override fun getItemId(position: Int): Long {
        return getItem(position).entity.name.hashCode().toLong()
    }

    inner class SummonerInformationHolder(binding: HolderSummonerInformationBinding) : BaseHolder<HolderSummonerInformationBinding, SummonerInformation>(binding) {
        override fun bind(element: SummonerInformation) {
            super.bind(element)
            binding.information = element
        }
    }

    class SummonerInformationItemCallback() : DiffUtil.ItemCallback<SummonerInformation>() {
        override fun areItemsTheSame(oldItem: SummonerInformation, newItem: SummonerInformation): Boolean {
            return oldItem.entity.name == newItem.entity.name
        }

        override fun areContentsTheSame(oldItem: SummonerInformation, newItem: SummonerInformation): Boolean {
            return oldItem == newItem
        }
    }
}

 

2. 주입받는 객체에 @AndroidEntryPoint를 지정하고 주입받을 필드에 @Inject를 지정한다. (private, val 키워드를 사용할 수 없다. Hilt가 Class를 인스턴스화하고 주입하기 때문이다.)

@AndroidEntryPoint
class MainFragment : BaseFragment<FragmentMainBinding>(R.layout.fragment_main) {
    @Inject
    lateinit var summonerEntityRepository: SummonerEntityRepository

    @Inject
    lateinit var summonerInformationAdapter: SummonerInformationAdapter

    private val summonerEntityViewModel by viewModels<SummonerEntityViewModel>()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        summonerEntityViewModel.summonerInformationLiveData.observe(viewLifecycleOwner) {
            summonerInformationAdapter.submitList(it)
        }
    }

    override fun init() {
        initSupportActionBar()
        initSummonerWithEntityRecyclerView()
        initOnAdd()
    }

    private fun initSupportActionBar() {
        setSupportActionBar(binding.toolbar)
    }

    private fun initSummonerWithEntityRecyclerView() {
        with(binding.summonerWithEntityRecyclerView) {
            adapter = summonerInformationAdapter
            ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
                override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
                    return false
                }

                override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
                    if (viewHolder is SummonerInformationAdapter.SummonerInformationHolder) {
                        summonerEntityRepository.deleteSummonerEntity(viewHolder.element.entity)
                    }
                }
            }).attachToRecyclerView(this)
            addItemDecoration(GridSpacingItemDecoration(10))
        }
    }

    private fun initOnAdd() {
        binding.setOnAdd {
            findNavController().navigate(MainFragmentDirections.actionMainFragmentToSummonerEntityEditDialog(
                SummonerEntity()
            ))
        }
    }
}

Git (예제코드)

github.com/KangTaeJong98/Example/tree/main/Android/Hilt

 

KangTaeJong98/Example

My Example Code. Contribute to KangTaeJong98/Example development by creating an account on GitHub.

github.com

 

'Android (안드로이드) > Hilt' 카테고리의 다른 글

Android in A..Z - Hilt (Entry Point)  (0) 2021.03.30
Android in A..Z - Hilt (Component)  (0) 2021.03.18
Android in A..Z - Hilt (Module)  (0) 2021.03.18
Comments