Cover Photo by Roman Koval from Pexels
แรกเริ่มเดิมทีว่าจะลองเขียน Blog เกี่ยวกับ JetPack Compose อย่างเดียว แต่ทำไปทำมาตอนลองทำ Sample Project แล้วมันดันมีจุดที่น่าสนใจหลายอย่างเลยคิดว่าถ้าเราเขียนเรื่อง JetPack Compose อย่างเดียวคงไม่จบ เลยอยากจะเขียนบทความนี้แยกเป็นหลายๆส่วน โดย Follow ตามตัวอย่าง code แบ่งตามเรื่องที่น่าสนใจดังนี้
- Hilt
- JetPack Compose (UI Component) & Unidirectional Data Flow
- จัดการ ใน JetPack Compose ด้วย LiveData และ State
- จัดการ ใน JetPack Compose ด้วย State/Share Flow
Hilt
Hilt คือ Library ที่ Google เพิ่งจะออกมาสดๆร้อนๆ โดยถูกสร้าง on-top ด้วย Dagger2 อีกที ใช้สำหรับการทำ Dependency Injection ใน Android ให้ง่ายขึ้น เรียกว่าเป็น Simplified version ของ Dagger Android ก็ว่าได้
ทำไมต้องทำ Dependency Injection
เพื่อให้เข้าใจภาพแบบง่ายขึ้น ยกตัวอย่าง สมมติเรามี class ที่ต้องทำหน้าที่ส่งข้อความโดยแรกเริ่มเดิมที่ ข้อความที่ส่งไปมันก็เป็น email ธรรมดา โดย function จะ return True หรือ False มาหากส่งข้อความสำเร็จหรือไม่สำเร็จ
class EmailSender {
fun sendEmail(message: String): Boolean {
// TO-DO email sending implementation is processed here
}
}
โดยเจ้า Class นี้ก็จะถูกเรียกใช้งานในอีก Layer นึง
class MessageProcessor {
private val emailSender = EmailSender()
fun sendProcessedMessage() {
val message = // doing many things to get message
emailSender.sendEmail(message)
}
}
class Main {
fun main() {
val processor = MessageProcessor()
processor.sendProcessedMessage()
}
}
ถ้าดูจากการใช้งานแล้ว ในเคสนี้ก็ไม่น่ามีอะไร เจ้า MessageProcessor
ก็ไป process ข้อความมาเพื่อจะส่ง message จากนั้นก็ส่งผ่าน EmailSender
ก็จบ ดูแล้วก็ไม่น่าจะมีปัญหาตรงไหน 🤔
⚠️ ประเด็นอยู่ตรงที่ว่า ถ้าเราสังเกตุดีๆ เราจะเห็นว่า MessageProcessor
นั้นมัน depends
กับตัว EmailSender
อยู่ ถ้าเราสร้าง class MessageProcessor
มาเมื่อไหร่ ตัว EmailSender
ก็จะถูกสร้างมาด้วยอัตโนมัติ ให้เราลองนึกถึงการเขียน Test เจ้าตัว MessageProcessor
คงเป็นเรื่องยากพอสมควร
สมมติว่าเราสามารถเขียน Test มันได้ แล้ววันนึง Requirement มันเปลี่ยนแทนที่จะต้องส่ง email กลายเป็นว่าต้องส่ง sms แทนล่ะ เท่ากับว่า Test ที่เราเขียนไปอาจจะต้องแก้พอสมควรเลย แบบนี้ไม่ดีแน่ สิ่งที่เราควรทำคือ ลด dependency ระหว่าง class แต่ละ class ลงซะ เลยเป็นที่มาของการทำ Dependency Injection
ในเคสนี้จะยกตัวอย่างการทำ Constructor Injection เนื่องจากว่าเป็นวิธีที่ง่ายต่อความเข้าใจรวมถึงง่ายต่อการเทสกว่า มากๆ
สร้าง interface MessageSender
interface MessageSender {
fun send(message: String): Boolean
}
ให้ class EmailSender
extends MessageSender
class EmailSender: MessageSender {
override fun send(message: String): Boolean {
// TO-DO email sending implementation is processed here
}
}
ให้ class SmsSender
extends MessageSender
class SmsSender: MessageSender {
override fun send(message: String): Boolean {
// TO-DO sms sending implementation is processed here
}
}
inject MessageSender ไปที่ constructor ของ MessageProcessor
class MessageProcessor internal constructor(
private val msgSender: MessageSender
) {
fun sendProcessedMessage() {
val message = // doing many things to get message
msgSender.send(message)
}
}
class Main {
fun main() {
val processor = MessageProcessor(SmsSender())
processor.sendProcessedMessage()
}
}
Unit Test ของ MessageProcessor
class MessageProcessorTest {
private val msgSender: MessageSender = mock()
private val processor = MessageProcessor(msgSender)
@Test
fun `test sendProcessedMessage(), msgSender send() is called`() {
processor.sendProcessedMessage()
Mockito.verify(msgSender.send(anyString()))
}
}
จริงๆ หลักการของ Dependency Injection มีง่ายๆแค่นี้ Dagger2, DaggerAndroid และ Hilt แค่ช่วยให้เราสามารถทำงานได้ง่ายขึ้นบน Android เท่านั้นเอง
Setup Hilt
จริงๆ วิธีการ Setup นั้นไม่ยากเลย โดย Google ได้อธิบายเป็น Step เอาไว้ให้เราแล้ว สามารถเข้าไปทำตามได้ที่นี่ ทั้งนี้ทั้งนั้นเผื่อคนที่ไม่อยาก Switch ไปอีกหน้า ผมจะเขียนวิธีการตาม Google เอาไว้ในนี้เลยละกัน
- ไปที่ file
build.gradle
ที่ Root ของ Project เราแล้วใส่
buildscript {
...
dependencies {
...
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}
- ไปที่ file
build.gradle
ของ module ที่ต้องการใช้ Hilt ในเคสนี้คือapp/build.gradle
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
...
}
dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}
Hilt ใช้ feature ของ Java8 จึงต้องประกาศด้วย
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
Sync Project ให้เรียบร้อย หลังจากนั้นทำการสร้าง Application class ของเราขึ้นมาแล้วใส่ @HiltAndroidApp
ให้เรียบร้อย
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class ComposePlaygroundApplication: Application()
ใน Activity ของเราใส่ @AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : AppCompatActivity()
เท่านี้เราก็พร้อมใช้งาน Hilt แล้ว
เทียบกับ Dagger
ใน Hilt เราเพียงใส่ @HiltAndroidApp
ที่ Application Class ในขณะที่
Dagger2 เราอาจจะต้องสร้าง Component
class ที่มี fun inject(activity: MainActivity)
ข้างใน หรือถ้าใครใช้ Dagger-Android วิธีการอาจจะซับซ้อนกว่านี้พอควร ในขณะที่ Hilt เพียงแค่เราประกาศ annotation ก็เพียงพอแล้ว
Field Injection
ใน Hilt นั้น การ inject นั้นง่ายมากๆ ยกตัวอย่างให้เห็นภาพ เรามี Activity class แล้วต้องการจะ inject class นึงเข้าไปใน Activity สมมติง่ายๆว่าเป็น EmailSender
class ละกัน
Activity ของเรา
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
}
class สำหรับส่ง email
class EmailSender {
fun send(msg: String) {
println("$msg is sent")
}
}
ถ้าเราต้องการ inject EmailSender
เข้าไปใน Activity เราสามารถประกาศ @Inject
ที่ Constructor ได้เลยตามนี้
class EmailSender
+ @Inject constructor() {
fun send(msg: String) {
println("$msg is sent")
}
}
ภายใน Activity เราสามารถประกาศ
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
+ @Inject
+ laterinit var emailSender: EmailSender
}
เพียงเท่านี้ EmailSender
ก็พร้อมใช้งานใน MainActivity
ของเรา
Constructor Injection
จากตัวอย่างข้างบน สมมติเราต้องการจะ format message ก่อนจะส่งโดยเรียกใช้class MessageFormatter โดยเราต้องการ Inject MessageFormatter
เข้าไปใน EmailSender
ผ่านทาง Constructor เราก็สามารถทำได้ยกตัวอย่าง
class สำหรับ format ข้อความ
class MessageFormatter {
fun formatMessages(msg: String): String {
return "formatted $msg"
}
}
เพิ่ม @Inject constructor()
ไปที่ MessageFormatter
class MessageFormatter
+ @Inject constructor() {
fun format(msg: String): String {
return "formatted $msg"
}
}
class EmailSender
@Inject constructor(
+ private val formatter: MessageFormatter
) {
fun send(msg: String) {
println("${formatter.format(msg)}is sent")
}
}
ปัญหา Injection กับ Interface
จากตัวอย่างข้างบนเราทำการ inject class ตรงๆ เข้าไปใน constructor ซึ่งสามารถทำได้ แต่ในหลายๆครั้ง เราต้องการให้ code ของเรามีความ abstract มากขึ้น เพื่อง่ายต่อการ Test วิธีการส่วนมากที่เราทำกันนั้นคือการสร้าง interface มาครอบ class ของเราอีกที ซึ่งใน Hilt ไม่สามารถทำได้ตรงๆตามวิธีการข้างบน
ยกตัวอย่าง:
เรามี Class ที่ชื่อ EmailSender สำหรับส่ง email อยู่แล้วโดยเราต้องการที่จะ Format ข้อความเราก่อนจะส่ง โดยในที่นี้เราจะสร้าง Interface มาเพื่อทำการ format ข้อความชื่อ MessageFormatter
สร้าง Interface ชื่อ MessageFormatter
interface MessageFormatter {
fun format(msg: String): String
}
สร้าง class ชื่อ MessageFormatterV1Impl ที่สืบทอดมาจาก MessageFormatter
class MessageFormatterV1Impl
@Inject constructor(): MessageFormatter {
override fun format(msg: String): String {
return "formatted $msg"
}
}
สร้าง class ชื่อ MessageFormatterV2Impl ที่สืบทอดมาจาก MessageFormatter
class MessageFormatterV2Impl
@Inject constructor(): MessageFormatter {
override fun format(msg: String): String {
return "$msg is formatted"
}
}
เรามี 2 classes ที่ extend มาจาก MessageFormatter
ชื่อ MessageFormatterV1Impl
และ MessageFormatterV2Impl
โดยทั้งสอง class ทำหน้าที่ format ข้อความเหมือนกัน แต่วิธีการ format ข้อความต่างกัน
inject MessageFormatter
ที่เป็น Inteface เข้าไปใน constructor ของ EmailSender
class EmailSender
@Inject constructor(
+ private val formatter: MessageFormatter
) {
fun send(msg: String) {
println("${formatter.format(msg)}is sent")
}
}
Error หลังจาก Build:
error: [Dagger/MissingBinding] MessageFormatter cannot be provided without an @Provides-annotated method.
public abstract static class ApplicationC implements ComposePlaygroundApplication_GeneratedInjector,
^
จริงๆ Error นั้นเรียกว่า make sense มากๆ ตรงที่ code ของเราจะรู้ได้ยังไงว่าจะเอา instant ไหน inject เข้าไปใน Constructor กันล่ะในเมื่อมี 2 classes ที่ implement interface เดียวกัน ซึ่งวิธีการแก้ปัญหานั้นอาจจะต้องใช้ตัวช่วยสักหน่อย นั่นก็คือการสร้าง @Module
หมายเหตุ 💡 ข้อดีของการทำ Constructor injection คือเราไม่จำเป็นต้องทำอะไรเพิ่มเติมเลยหากเราต้องการจะเขียน UnitTest ไม่ใช่แค่สำหรับ Hilt แต่ Dagger ด้วยก็เหมือนกัน โดยเฉพาะอย่างยิ่งการทำ Constructor injection ด้วย Interface นั้นทำให้การ Mock เป็นเรื่องง่ายมากๆครับ
Component
ก่อนจะเริ่มสร้าง Module นั้น สิ่งที่เราต้องทำความเข้าใจก่อนคือ Component และ Component Scope ตัว Hilt เองนั้นจะมีการ pre-create Hilt Components มาให้เราใช้งานแล้วโดยที่เราไม่จำเป็นต้องไปสร้าง Component เองเหมือนที่เราทำใน Dagger โดยสามารถแบ่งตามนี้
ถึงตรงนี้หลายคนอาจจะงงว่า Component นั้นคืออะไร ถ้าพูดให้เข้าใจง่ายคือ เปรียบเสมือน Layer ที่เราอยากจะให้ instance ที่เราสร้างเข้าไปอยู่ ยกตัวอย่าง เราอยากจะให้ EmailSender
class ของเราใช้ได้ Activity เราก็เพียงแค่ต้องมั่นใจว่า มันถูก "ใส่" เข้าไปใน (@InstallIn
) ActivityComponent
แล้วเมื่อไหร่ที่ instance เราถูกสร้างและถูกทำลายล่ะ?
instance ที่ถูก bound เข้าไปจะถูกสร้างและทำลายตามแต่ละ Component ที่ตัวมันเองโดน installed ลงไป
Scope
โดยปกติแล้ว instance ที่ถูกสร้างขึ้นมาจะไม่มีการกำหนด scope ของการใช้งาน (unscoped) นั่นหมายความว่า ทุกๆครั้งที่มีการเรียกใช้ class นั้น instance นั้นจะถูกสร้างขึ้นมาใหม่ ยกตัวอย่าง:
เรามี class EmailSender
class EmailSender @Inject constructor() {
fun send(msg: String){
println(msg)
}
}
ทำการ inject EmailSender ในอีก class นึงผ่านทาง Constructor ชื่อว่า MessageHandler แล้วลอง Print ค่าออกมาดู
class MessageHandler @Inject constructor(
private val sender: EmailSender
) {
init {
println("emailSender from ${this::class.java} $sender" )
}
}
ทำการ inject ทั้ง EmailSender และ MessageHandler ใน Activity
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var emailSender: EmailSender
@Inject
lateinit var messageHandler: MessageHandler
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
println("emailSender from ${this::class.java} $emailSender" )
}
}
- เรา Inject
MessageHandler
เพื่อให้ Hilt สร้าง instance ของ MessageHandler เพื่อดูค่าEmailSender
ที่เรา print ใน init ของ MessageHandler - เรา Inject
EmailSender
เพื่อให้ Hilt สร้าง instance ของEmailSender
เพื่อดูค่าEmailSender
ที่เรา print ใน onCreate ของ Activity
ค่าที่ได้:
2021-01-04 16:13:03.060 7374-7374/? I/System.out: emailSender from class MessageHandler EmailSender@cacc883
2021-01-04 16:13:03.102 7374-7374/? I/System.out: emailSender from class MainActivity EmailSender@72ec68a
จะเห็นว่า Instance นั้นเป็นคนละตัวกัน
ลองกำหนด Scope ลงไปใน EmailSender ในเคสนี้จะลองกำหนด @ActivityScoped
+ @ActivityScoped
class EmailSender @Inject constructor() {
fun send(msg: String){
println(msg)
}
}
ค่าที่ได้:
2021-01-04 16:16:06.611 7714-7714/? I/System.out: emailSender from MessageHandler EmailSender@b6f3903
2021-01-04 16:16:06.634 7714-7714/? I/System.out: emailSender from class MainActivity EmailSender@b6f3903
Scope เปรียบเสมือนขอบเขตของการใช้งานว่าเราอยากจะให้ Instance นั้นถูกใช้ได้ในขอบเขตไหนลงไปเป็นลำดับขั้น โดย Hilt เอง ก็ได้ทำการ Predefined เอาไว้แล้วตามนี้
ยกตัวอย่าง:
ตัวอย่างที่ 1 ต้องการให้ class EmailSender
ใช้งานได้ใน Activity
และให้ class EmailSender
และมีขอบเขตการใช้งานภายใน Activity ลงไป
เทียบตาราง:
- ต้องให้
class EmailSender
ถูก Inject ในActivity
- ต้องให้
class EmailSender
มี Scope เป็นActivityScope
ตัวอย่าง Code:
ประกาศ @ActivityScoped
@ActivityScoped
class EmailSender @Inject constructor() {
fun send(msg: String) {
println("msg is sent")
}
}
ใช้งาน EmailSender ใน Activity
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var emailSender: EmailSender
}
ตัวอย่างที่ 2 ต้องการให้ class EmailSender
ใช้งานได้ใน Fragment
และให้ class EmailSender
และมีขอบเขตการใช้งานภายใน Fragment
ลงไป
เทียบตาราง:
- ต้องให้
class EmailSender
ถูก Inject ในFragment
- ต้องให้
class EmailSender
มี Scope เป็นFragmentScope
ตัวอย่าง Code:
ประกาศ @FragmentScoped
@FragmentScoped
class EmailSender @Inject constructor() {
fun send(msg: String) {
println("msg is sent")
}
}
ใช้งาน EmailSender ใน Fragment
@AndroidEntryPoint
class MainFragment : Fragment() {
@Inject
lateinit var emailSender: EmailSender
}
ตัวอย่างที่ 3 ต้องการให้ class EmailSender
ใช้งานได้ใน Fragment
แต่ยังให้ class EmailSender
และมีขอบเขตการใช้งานภายใน Activity ลงไป
เทียบตาราง:
- ต้องให้
class EmailSender
ถูก Inject ในFragment
- ต้องให้
class EmailSender
มี Scope เป็นActivityScope
ตัวอย่าง Code:
ประกาศ @ActivityScoped
@ActivityScoped
class EmailSender @Inject constructor() {
fun send(msg: String) {
println("msg is sent")
}
}
ใช้งาน EmailSender ใน Fragment
@AndroidEntryPoint
class MyFragment : Fragment() {
@Inject
lateinit var emailSender: EmailSender
}
เนื่องจากว่า ขอบเขตการใช้งานของ EmailSender อยู่ใน Activity แต่การใช้งานเรานำไปใช้ใน Fragment ซึ่งอยู่ภายใต้ Activity อีกที ในกรณีนี้นั้นสามารถทำได้
ข้อควรระวัง Scope กับ Component นั้นมีความสัมพันธ์กันเป็นลำดับชั้น ถ้าหากเราประกาศ Scope กับ Component ไม่ถูกต้องเราอาจจะเจอ Error ได้ยกตัวอย่าง:
- ให้
class EmailSender
ถูกใช้งานใน Activity - แต่ให้
class EmailSender
มี Scope เป็นFragmentScope
เพราะต้องการให้Class EmailSender
มีขอบเขตการใช้งานแค่ภายในFragment
Compile Error ที่เกิดขึ้น:
error: [Dagger/IncompatiblyScopedBindings] ComposePlaygroundApplication_HiltComponents.ActivityC scoped with @dagger.hilt.android.scopes.ActivityScoped may not reference bindings with different scopes:
public abstract static class ApplicationC implements ...,
แก้ไขปัญหา Constructor Injection กับ Interface
จากที่บอกไปด้านบนคือ วิธีการแก้ไขนั้นทำได้ไม่ยากโดยการใช้ Module
ซึ่งวิธีการใช้งาน Module ของ Hilt นั้นมี 2 วิธีหลักๆ คือการ @Binds
และ @Provides
Binds module
วิธีการ bind instance ด้วยการใช้ @Binds
ค่อนข้างเรียบง่ายและเหมาะกับการแก้ปัญหา constructor injection ด้วย interface ในกรณีที่เรามี class ที่เราต้องการ bind instance เข้าไป ยกตัวอย่าง:
เรามี MessageFormatter ที่เป็น interface และมี class MessageFormatterImpl ที่เป็น class
interface MessageFormatter {
fun format(msg: String): String
}
class MessageFormatterImpl: MessageFormatter {
fun format(msg: String): String {
return "Formatted $msg"
}
}
ต้องการ inject เข้าไปที่ constructor ของ EmailSender class
class EmailSender
@Inject constructor(
private val formatter: MessageFormatter
)
เราต้องการ Inject EmailSender
เข้าไปที่ Activity โดยที่ Compile ผ่าน
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var emailSender: EmailSender
}
เริ่มทำการสร้าง Module ในที่นี้จะให้ชื่อว่า MessageSendingModule
@Module
abstract class MessageSendingModule {
}
ทำการประกาศว่าจะทำ Module ไปใช้ที่ไหน ในที่นี้ เราต้องการใช้ EmailSender
ที่ Activity เราจึงต้องประกาศ @InstallIn(ActivityComponent::class)
+ @InstallIn(ActivityComponent::class)
@Module
abstract class MessageSendingModule {
}
ทำการ Bind Instance ของ MessageFormatter
@InstallIn(ActivityComponent::class)
@Module
abstract class MessageSendingModule {
+ @Binds
+ fun bindMessageFormatter(impl: MessageFormatterImpl): MessageFormatter
}
จุดสังเกตุ ⚠️⚠️ เราใส่ MessageFormatterImpl
ที่เป็น class
ลงไปเป็น parameter ของ bindMessageFormatter
และให้ function return MessageFormatter
ที่เป็น Interface
ทำการประกาศ Scope ของ MessageFormatter
เคสนี้ เราต้องการให้มันมีขอบเขตการใช้งานภายใน Activity ลงไป ดังนั้นต้องใช้ @ActivityScope
@InstallIn(ActivityComponent::class)
@Module
abstract class MessageSendingModule {
@Binds
+ @ActivityScope
fun bindMessageFormatter(impl: MessageFormatterImpl): MessageFormatter
}
เพียงเท่านี้เราก็สามารถ compile ได้ผ่านฉลุย 🚀🚀🚀
Provide module
Provide module มักใช้ในกรณีที่เราต้องการสร้าง Instance ของ Class ด้วยเราเอง ยกตัวอย่าง หากเราต้องการ Inject Gson
instance การใช้ @Binds ตามตัวอย่างข้างบนไม่สามารถทำได้ วิธีการใช้ Provide module จึงเหมาะสมมากกว่า
ตัวอย่าง:
- ต้องการให้
Gson
เข้าถูกใช้งานที่ใดก็ได้ใน Application - ต้องการให้ Gson มีขอบเขตการใช้งานภายใน Application
สร้าง module ในที่นี้จะให้ชื่อว่า GsonModule
@Module
class GsonModule
จุดสังเกตุ ⚠️ class GsonModule ไม่ใช่ abstract class เหมือนในกรณี @Binds
ประกาศ @InstallIn(ApplicationComponent::class)
เนื่องจากว่าเราต้องการให้ Gson ถูกใช้งานที่ Application level ในกรณีนี้ instance ของ Gson จะถูกนำไปใช้งานได้ทุกที่ของ App (หาก inject Gson ด้วย Hilt)
+ @InstallIn(ApplicationComponent::class)
@Module
class GsonModule
สร้าง Function ที่ทำหน้าที่ Provide Gson
@InstallIn(ApplicationComponent::class)
@Module
class GsonModule {
+ fun provideGson(): Gson = GsonBuilder().create()
}
ประกาศ @Singleton
เพื่อให้ Gson มี มีขอบเขตการใช้งานภายที่ Application ลงไป
จากนั้นประกาศ @Provides
@InstallIn(ApplicationComponent::class)
@Module
class GsonModule {
+ @Singleton
+ @Provides
fun provideGson(): Gson = GsonBuilder().create()
}
เพียงเท่านี้เราก็สามารถ Inject Gson ไปที่ไหนก็ได้ใน App เพราะ Gson จะมีขอบเขตการใช้งานที่ Application layer ลงไป
หมายเหตุ 💡 วิธีการสร้าง Module จำเป็นต้องให้ component ที่เราจะระบุใน @InstallIn
ตรงกันกับ Scope
รวมถึงการนำไปใช้งาน (inject) ด้วย ตามตาราง:
เช่น:
ต้องการ EmailSender class ใช้งานใน Fragment ลงไป (Fragment, View, Service) เราสามารถระบุ
-
@InstallIn
ApplicationComonent
หรือAcivityComponent
หรือFragmentComponent
ได้ - ระบุ Scope เป็น
@Singleton
หรือ@ActivityScope
หรือ@FragmentScope
โดยต้องตรงกับ Component ที่ InstallIn - สามารถ inject EmailSender ได้ใน Fragment, View และ Service
Monolithic components
อีกเรื่องที่สำคัญสำหรับ Hilt คือ การที่ Hilt นั้นมีโครงสร้าง แบบ Monolithic components นั่นหมายความว่า ของที่อยู่ใน Components จะถูกใช้ได้ในทุกที่ใน Layer เดียวกัน
ยกตัวอย่าง:
- Application เรามี 3 Activities ชื่อ
MainActivity
,SecondActivity
,ThirdActivity
โดยให้ทั้งสาม Activities สามารถใช้งาน Hilt ได้โดยการระบุ@AndroidEntryPoint
บน Activities ทั้ง 3 ตัว
@AndroidEntryPoint
class ThirdActivity : AppCompatActivity()
@AndroidEntryPoint
class SecondActivity : AppCompatActivity()
@AndroidEntryPoint
class MainActivity : AppCompatActivity()
- ให้
EmailSender
มี Scope เป็นActivityScoped
@ActivityScoped
class EmailSender @Inject constructor() {
fun send(msg: String){
println(msg)
}
}
inject EmailSender
ในทั้ง 3 Activities
@AndroidEntryPoint
class ThirdActivity : AppCompatActivity() {
@Inject
lateinit var emailSender: EmailSender
}
@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {
@Inject
lateinit var emailSender: EmailSender
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var emailSender: EmailSender
}
ทั้งนี้ทั้งนั้น ถึงแม้ว่าเราจะ inject มันได้ ทั้ง 3 ที่ แต่ instance ก็ยังคงเป็นคนละตัวกัน
ทดสอบด้วยการ Print ออกมาใน onCreate ของแต่ละ Activity:
println("emailSender from ${this::class.java} $emailSender" )
ผลที่ได้
2021-01-04 14:51:01.868 5198-5198/? I/System.out: emailSender from class MainActivity EmailSender@245bf98
2021-01-04 14:51:02.346 5198-5198/? I/System.out: emailSender from class SecondActivity EmailSender@50720c3
2021-01-04 14:51:02.675 5198-5198/? I/System.out: emailSender from class ThirdActivity EmailSender@2026848
ลองเปลี่ยนให้ Scope เป็น @Singleton
แทน
- @ActivityScoped
+ @Singleton
class EmailSender @Inject constructor() {
fun send(msg: String){
println(msg)
}
}
ผลที่ได้
2021-01-04 14:51:01.868 5198-5198/? I/System.out: emailSender from class MainActivity EmailSender@245bf98
2021-01-04 14:51:02.346 5198-5198/? I/System.out: emailSender from class SecondActivity EmailSender@245bf98
2021-01-04 14:51:02.675 5198-5198/? I/System.out: emailSender from class ThirdActivity EmailSender@245bf98
ทีนี้เรามาลองอะไรเล่นๆดูบ้าง จากตัวอย่าง code ด้านบน ให้ scope เป็น @ActivityRetainedScoped
ดู
@ActivityRetainedScoped
class EmailSender @Inject constructor() {
fun send(msg: String){
println(msg)
}
}
ผลที่ได้
2021-01-04 14:51:01.868 5198-5198/? I/System.out: emailSender from class MainActivity EmailSender@3ade714
2021-01-04 14:51:02.346 5198-5198/? I/System.out: emailSender from class SecondActivity EmailSender@5776b47
2021-01-04 14:51:02.675 5198-5198/? I/System.out: emailSender from class ThirdActivity EmailSender@8b35fe6
จะเห็นว่า Instance นั้นยังตงเป็นคนละตัวกัน เนื่องจากว่า เรามี @AndroidEntryPoint
อยู่สามที่นั่นทำให้ Hilt ทำการสร้าง Instance ของ emailSender แยกกัน
ลองใช้ @ActivityRetainedScoped
เหมือนเดิม แต่เปลี่ยนให้มี @AndroidEntryPoint
เพียงที่เดียวบ้าง
@ActivityRetainedScoped
class EmailSender @Inject constructor() {
fun send(msg: String){
println(msg)
}
}
สร้าง ClassA และ ClassB โดย inject emailSender
ไปที่ constructor
class ClassA @Inject constructor(
private val emailSender: EmailSender
) {
init {
println("From $this EmailSender is $emailSender")
}
}
class ClassB @Inject constructor(
private val emailSender: EmailSender
) {
init {
println("From $this EmailSender is $emailSender")
}
}
Print ค่าใน Activity:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var emailSender: EmailSender
@Inject
lateinit var classA: ClassA
@Inject
lateinit var classB: ClassB
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
println("From $this EmailSender is $emailSender")
}
}
ผลที่ได้
2021-01-05 10:33:21.296 13089-13089 From ClassA@5d9e5b2 EmailSender is EmailSender@b6f3903
2021-01-05 10:33:21.296 13089-13089 From ClassB@d0c6080 EmailSender is EmailSender@b6f3903
2021-01-05 10:33:21.362 13089-13089 From MainActivity@fa313f1 EmailSender is EmailSender@b6f3903
จบไปคร่าวๆ สำหรับเรื่อง Hiltเบื้องต้น หวังว่าจะนำไปใช้งานกันได้ไม่ยาก ส่วน Part หน้าจะมาพูดถึง JetPack Compose บ้าง ว่ามัน Revolutionize วิธีการเขียน UI ของ Android App เราอย่างไร
Top comments (0)