สถาปัตยกรรมแอปที่แนะนำของ Android แนะนำให้แบ่งโค้ดออกเป็นคลาสต่างๆ เพื่อใช้ประโยชน์จากการแยกข้อกังวล ซึ่งเป็นหลักการที่แต่ละคลาสในลําดับชั้นมีหน้าที่ที่กําหนดไว้เพียงอย่าง��ดียว ซึ่งส่งผลให้มีคลาสขนาดเล็กจำนวนมากขึ้นที่ต้องเชื่อมต่อกันเพื่อตอบสนองความต้องการซึ่งกันและกัน

ความสัมพันธ์แบบพึ่งพาระหว่างคลาสสามารถแสดงเป็นกราฟ ซึ่งแต่ละคลาสจะเชื่อมต่อกับคลาสที่ตนพึ่งพา การนําเสนอคลาสทั้งหมดและ Dependency ของคลาสประกอบกันเป็นกราฟแอปพลิเคชัน
ในรูปที่ 1 คุณจะเห็นภาพรวมของกราฟแอปพลิเคชัน
เมื่อคลาส A (ViewModel
) ขึ้นอยู่กับคลาส B (Repository
) จะมีเส้นที่ชี้จาก A ไปยัง B เพื่อแสดงความสัมพันธ์นั้น
Dependency Injection ช่วยสร้างการเชื่อมต่อเหล่านี้และให้คุณเปลี่ยนการใช้งานเพื่อทดสอบได้ เช่น เมื่อทดสอบ ViewModel
ที่ขึ้นอยู่กับที่เก็บข้อมูล คุณสามารถส่งผ่านการใช้งาน Repository
แบบต่างๆ ด้วยข้อมูลจำลองหรือข้อมูลจำลองเพื่อทดสอบกรณีต่างๆ ได้
ข้อมูลเบื้องต้นเกี่ยวกับการนําเข้าข้อมูล Dependency ด้วยตนเอง
ส่วนนี้จะอธิบายวิธีใช้การฉีดข้อมูล Dependency ด้วยตนเองในสถานการณ์จริงของแอป Android บทแนะนำนี้จะอธิบายแนวทางแบบซ้ำๆ ที่คุณอาจเริ่มต้นใช้การฉีดข้อมูลในแอป โดยแนวทางนี้จะปรับปรุงไปเรื่อยๆ จนกว่าจะถึงจุดที่คล้ายกับสิ่งที่ Dagger จะสร้างให้คุณโดยอัตโนมัติ ดูข้อมูลเพิ่มเติมเกี่ยวกับ Dagger ได้ที่ข้อมูลเบื้องต้นเกี่ยวกับ Dagger
ขั้นตอนคือกลุ่มหน้าจอในแอปที่สอดคล้องกับฟีเจอร์ ตัวอย่��งของขั้นตอน ได้แก่ การเข้าสู่ระบบ การลงทะเบียน และการชำระเงิน
เมื่อครอบคลุมขั้นตอนการเข้าสู่ระบบสําหรับแอป Android ทั่วไป LoginActivity
จะขึ้นอยู่กับ LoginViewModel
ซึ่งก็ขึ้นอยู่กับ UserRepository
จากนั้น UserRepository
จะขึ้นอยู่กับ UserLocalDataSource
และ
UserRemoteDataSource
ซึ่งก็ขึ้นอยู่กับบริการ Retrofit

LoginActivity
คือจุดแรกเข้าของขั้นตอนการเข้าสู่ระบบและผู้ใช้โต้ตอบกับกิจกรรม ดังนั้น LoginActivity
จึงต้องสร้าง
LoginViewModel
พร้อมทั้งสร้างการพึ่งพาทั้งหมด
คลาส Repository
และ DataSource
ของโฟลว์จะมีลักษณะดังนี้
Kotlin
class UserRepository( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... } class UserLocalDataSource { ... } class UserRemoteDataSource( private val loginService: LoginRetrofitService ) { ... }
Java
class UserLocalDataSource { public UserLocalDataSource() { } ... } class UserRemoteDataSource { private final Retrofit retrofit; public UserRemoteDataSource(Retrofit retrofit) { this.retrofit = retrofit; } ... } class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } ... }
LoginActivity
จะมีลักษณะดังนี้
Kotlin
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // In order to satisfy the dependencies of LoginViewModel, you have to also // satisfy the dependencies of all of its dependencies recursively. // First, create retrofit which is the dependency of UserRemoteDataSource val retrofit = Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService::class.java) // Then, satisfy the dependencies of UserRepository val remoteDataSource = UserRemoteDataSource(retrofit) val localDataSource = UserLocalDataSource() // Now you can create an instance of UserRepository that LoginViewModel needs val userRepository = UserRepository(localDataSource, remoteDataSource) // Lastly, create an instance of LoginViewModel with userRepository loginViewModel = LoginViewModel(userRepository) } }
Java
public class MainActivity extends Activity { private LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // In order to satisfy the dependencies of LoginViewModel, you have to also // satisfy the dependencies of all of its dependencies recursively. // First, create retrofit which is the dependency of UserRemoteDataSource Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService.class); // Then, satisfy the dependencies of UserRepository UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit); UserLocalDataSource localDataSource = new UserLocalDataSource(); // Now you can create an instance of UserRepository that LoginViewModel needs UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); // Lastly, create an instance of LoginViewModel with userRepository loginViewModel = new LoginViewModel(userRepository); } }
แนวทางนี้มีปัญหาดังนี้
มีโค้ดที่ต้องเขียนซ้ำจำนวนมาก หากต้องการสร้างอินสแตนซ์อื่นของ
LoginViewModel
ในส่วนอื่นของโค้ด คุณจะต้องเขียนโค้ดซ้ำต้องประกาศการพึ่งพาตามลําดับ คุณต้องสร้าง экземпляр
UserRepository
ก่อนLoginViewModel
จึงจะสร้างได้การนำออบเจ็กต์มาใช้ซ้ำทำได้ยาก หากต้องการนํา
UserRepository
ไปใช้ซ้ำในหลายฟีเจอร์ คุณจะต้องทําให้เป็นไปตามรูปแบบ Singleton รูปแบบ Singleton ทําให้การทด��อบทําได้ยากขึ้นเนื่องจากการทดสอบทั้งหมดใช้อินสแตนซ์ Singleton เดียวกัน
การจัดการทรัพยากร Dependency ด้วยคอนเทนเนอร์
หากต้องการแก้ปัญหาการนําออบเจ็กต์มาใช้ซ้ำ คุณสามารถสร้างคลาสคอนเทนเนอร์ของ Dependency ของคุณเองเพื่อใช้รับ Dependency อินสแตนซ์ทั้งหมดที่คอนเทนเนอร์นี้ให้ไว้อาจเป็นแบบสาธารณะได้ ในตัวอย่างนี้ เนื่องจากคุณต้องใช้เพียงอินสแตนซ์ของ UserRepository
คุณจึงทําให้ทรัพยากร Dependency เป็นแบบส่วนตัวได้โดยมีตัวเลือกในการทําให้เป็นแบบสาธารณะในอนาคตหากจําเป็นต้องระบุ
Kotlin
// Container of objects shared across the whole app class AppContainer { // Since you want to expose userRepository out of the container, you need to satisfy // its dependencies as you did before private val retrofit = Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService::class.java) private val remoteDataSource = UserRemoteDataSource(retrofit) private val localDataSource = UserLocalDataSource() // userRepository is not private; it'll be exposed val userRepository = UserRepository(localDataSource, remoteDataSource) }
Java
// Container of objects shared across the whole app public class AppContainer { // Since you want to expose userRepository out of the container, you need to satisfy // its dependencies as you did before private Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService.class); private UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit); private UserLocalDataSource localDataSource = new UserLocalDataSource(); // userRepository is not private; it'll be exposed public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); }
เนื่องจากมีการนําไปใช้ทั่วทั้งแอปพลิเคชัน จึงต้องวางไว้ในที่ที่กิจกรรมทั้งหมดใช้ได้ ซึ่งก็คือคลาส Application
สร้างคลาส Application
ที่กําหนดเองซึ่งมีอินสแตนซ์ AppContainer
Kotlin
// Custom Application class that needs to be specified // in the AndroidManifest.xml file class MyApplication : Application() { // Instance of AppContainer that will be used by all the Activities of the app val appContainer = AppContainer() }
Java
// Custom Application class that needs to be specified // in the AndroidManifest.xml file public class MyApplication extends Application { // Instance of AppContainer that will be used by all the Activities of the app public AppContainer appContainer = new AppContainer(); }
ตอนนี้คุณรับอินสแตนซ์ของ AppContainer
จากแอปพลิเคชันและรับอินสแตนซ์ UserRepository
ที่แชร์ได้แล้ว โดยทำดังนี้
Kotlin
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Gets userRepository from the instance of AppContainer in Application val appContainer = (application as MyApplication).appContainer loginViewModel = LoginViewModel(appContainer.userRepository) } }
Java
public class MainActivity extends Activity { private LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Gets userRepository from the instance of AppContainer in Application AppContainer appContainer = ((MyApplication) getApplication()).appContainer; loginViewModel = new LoginViewModel(appContainer.userRepository); } }
วิธีนี้จะทำให้คุณไม่มี UserRepository
แบบ Singleton แต่คุณจะมีAppContainer
ที่แชร์ในกิจกรรมทั้งหมดซึ่งมีออบเจ็กต์จากกราฟ และสร้างอินสแตนซ์ของออบเจ็กต์เหล่านั้นที่คลาสอื่นๆ สามารถใช้ได้
หากต้องใช้ LoginViewModel
ในตำแหน่งอื่นๆ ของแอปพลิเคชันด้วย คุณควรสร้างอินสแตนซ์ของ LoginViewModel
ในตำแหน่งที่รวมศูนย์ คุณสามารถย้ายการสร้าง LoginViewModel
ไปยังคอนเทนเนอร์และระบุออบเจ็กต์ใหม่ประเภทนั้นด้วย Factory โค้ดของ LoginViewModelFactory
จะมีลัก��ณะดังนี้
Kotlin
// Definition of a Factory interface with a function to create objects of a type interface Factory<T> { fun create(): T } // Factory for LoginViewModel. // Since LoginViewModel depends on UserRepository, in order to create instances of // LoginViewModel, you need an instance of UserRepository that you pass as a parameter. class LoginViewModelFactory(private val userRepository: UserRepository) : Factory{ override fun create(): LoginViewModel { return LoginViewModel(userRepository) } }
Java
// Definition of a Factory interface with a function to create objects of a type public interface Factory<T> { T create(); } // Factory for LoginViewModel. // Since LoginViewModel depends on UserRepository, in order to create instances of // LoginViewModel, you need an instance of UserRepository that you pass as a parameter. class LoginViewModelFactory implements Factory{ private final UserRepository userRepository; public LoginViewModelFactory(UserRepository userRepository) { this.userRepository = userRepository; } @Override public LoginViewModel create() { return new LoginViewModel(userRepository); } }
คุณสามารถใส่ LoginViewModelFactory
ไว้ใน AppContainer
และทําให้ LoginActivity
ใช้ LoginViewModelFactory
ดังนี้
Kotlin
// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory class AppContainer { ... val userRepository = UserRepository(localDataSource, remoteDataSource) val loginViewModelFactory = LoginViewModelFactory(userRepository) } class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Gets LoginViewModelFactory from the application instance of AppContainer // to create a new LoginViewModel instance val appContainer = (application as MyApplication).appContainer loginViewModel = appContainer.loginViewModelFactory.create() } }
Java
// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory public class AppContainer { ... public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); public LoginViewModelFactory loginViewModelFactory = new LoginViewModelFactory(userRepository); } public class MainActivity extends Activity { private LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Gets LoginViewModelFactory from the application instance of AppContainer // to create a new LoginViewModel instance AppContainer appContainer = ((MyApplication) getApplication()).appContainer; loginViewModel = appContainer.loginViewModelFactory.create(); } }
แนวทางนี้ดีกว่าแนวทางก่��นหน้า แต่ยังคงมีความท้าทายบางอย่างที่ควรพิจารณา ดังนี้
คุณต้องจัดการ
AppContainer
ด้วยตนเอง โดยสร้างอินสแตนซ์สำหรับข้อกําหนดทั้งหมดด้วยตนเองยังมีโค้ดที่ต้องเขียนซ้ำจำนวนมาก คุณต้องสร้าง Factory หรือพารามิเตอร์ด้วยตนเอง โดยขึ้นอยู่กับว่าคุณต้องการใช้ออบเจ็กต์ซ้ำหรือไ��่
การจัดการทรัพยากร Dependency ในงานของแอปพลิเคชัน
AppContainer
จะมีความซับซ้อนเมื่อคุณต้องการรวมฟังก์ชันการทำงานอื่นๆ ไว้ในโปรเจ็กต์ เมื่อแอปมีขนาดใหญ่ขึ้นและคุณเริ่มเปิดตัวขั้นตอนต่างๆ ของฟีเจอร์ ปัญหาที่อาจเกิดขึ้นก็จะยิ่งมากขึ้นไปอีก
เมื่อคุณมีขั้นตอนที่แตกต่างกัน คุณอาจต้องการให้ออบเจ็กต์อยู่ในขอบเขตของขั้นตอนนั้นๆ เท่านั้น ตัวอย่างเช่น เมื่อสร้าง
LoginUserData
(ซึ่งอาจประกอบด้วยชื่อผู้ใช้และรหัสผ่านที่ใช้ในขั้นตอนการเข้าสู่ระบบเท่านั้น) คุณไม่ต้องการให้เก็บข้อมูลจากขั้นตอนการเข้าสู่ระบบเดิมของผู้ใช้รายอื่น คุณต้องมีอินสแตนซ์ใหม่สำหรับแต่ละขั้นตอนใหม่ ซึ่งทำได้โดยการสร้างออบเจ็กต์FlowContainer
ภายในAppContainer
ดังที่แสดงในตัวอย่างโค้ดถัดไปการเพิ่มประสิทธิภาพกราฟแอปพลิเคชันและคอนเทนเนอร์โฟลว์ก็อาจเป็นเรื่องยากเช่นกัน อย่าลืมลบอินสแตนซ์ที่ไม่จําเป็นออก ทั้งนี้ขึ้นอยู่กับขั้นตอนที่คุณทําอยู่
ลองจินตนาการว่าคุณมีขั้นตอนการเข้าสู่ระบบที่ประกอบด้วยกิจกรรม 1 รายการ (LoginActivity
) และหลายส่วนที่แตกต่างออกไป (LoginUsernameFragment
และ LoginPasswordFragment
) มุมมองเหล่านี้ต้องการดำเนินการต่อไปนี้
เข้าถึงอินสแตนซ์
LoginUserData
เดียวกันที่ต้องแชร์จนกว่าขั้นตอนการเข้าสู่ระบบจะเสร็จสมบูรณ์สร้างอินสแตนซ์ใหม่ของ
LoginUserData
เมื่อขั้นตอนเริ่มต้นขึ้นอีกครั้ง
ซึ่งทำได้ด้วยคอนเทนเนอร์ขั้นตอนเข้าสู่ระบบ คอนเทนเนอร์นี้ต้องสร้างขึ้นเมื่อเริ่มขั้นตอนการเข้าสู่ระบบและนำออกจากหน่วยความจำเมื่อขั้นตอนสิ้นสุด
มาเพิ่ม LoginContainer
ลงในโค้ดตัวอย่างกัน คุณต้องการสร้างอินสแตนซ์ LoginContainer
หลายรายการในแอป ดังนั้นแทนที่จะสร้างเป็น Singleton ให้สร้างเป็นคลาสที่มี Dependency ที่ขั้นตอนเข้าสู่ระบบต้องการจาก AppContainer
Kotlin
class LoginContainer(val userRepository: UserRepository) { val loginData = LoginUserData() val loginViewModelFactory = LoginViewModelFactory(userRepository) } // AppContainer contains LoginContainer now class AppContainer { ... val userRepository = UserRepository(localDataSource, remoteDataSource) // LoginContainer will be null when the user is NOT in the login flow var loginContainer: LoginContainer? = null }
Java
// Container with Login-specific dependencies class LoginContainer { private final UserRepository userRepository; public LoginContainer(UserRepository userRepository) { this.userRepository = userRepository; loginViewModelFactory = new LoginViewModelFactory(userRepository); } public LoginUserData loginData = new LoginUserData(); public LoginViewModelFactory loginViewModelFactory; } // AppContainer contains LoginContainer now public class AppContainer { ... public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); // LoginContainer will be null when the user is NOT in the login flow public LoginContainer loginContainer; }
เมื่อคุณมีคอนเทนเนอร์สำหรับโฟลว์หนึ่งๆ แล้ว คุณจะต้องตัดสินใจว่าจะสร้างและลบอินสแตนซ์คอนเทนเนอร์เมื่อใด เนื่องจากขั้นตอนการเข้าสู่ระบบของคุณอยู่ในกิจกรรม (LoginActivity
) กิจกรรมดังกล่าวจึงเป็นผู้จัดการวงจรชีวิตของคอนเทนเนอร์นั้น LoginActivity
สร้างอินสแตนซ์ใน onCreate()
และลบใน onDestroy()
ได้
Kotlin
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel private lateinit var loginData: LoginUserData private lateinit var appContainer: AppContainer override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) appContainer = (application as MyApplication).appContainer // Login flow has started. Populate loginContainer in AppContainer appContainer.loginContainer = LoginContainer(appContainer.userRepository) loginViewModel = appContainer.loginContainer.loginViewModelFactory.create() loginData = appContainer.loginContainer.loginData } override fun onDestroy() { // Login flow is finishing // Removing the instance of loginContainer in the AppContainer appContainer.loginContainer = null super.onDestroy() } }
Java
public class LoginActivity extends Activity { private LoginViewModel loginViewModel; private LoginData loginData; private AppContainer appContainer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); appContainer = ((MyApplication) getApplication()).appContainer; // Login flow has started. Populate loginContainer in AppContainer appContainer.loginContainer = new LoginContainer(appContainer.userRepository); loginViewModel = appContainer.loginContainer.loginViewModelFactory.create(); loginData = appContainer.loginContainer.loginData; } @Override protected void onDestroy() { // Login flow is finishing // Removing the instance of loginContainer in the AppContainer appContainer.loginContainer = null; super.onDestroy(); } }
เช่นเดียวกับ LoginActivity
ข้อมูลโค้ดการเข้าสู่ระบบจะเข้าถึง LoginContainer
จาก AppContainer
และใช้อินสแตนซ์ LoginUserData
ที่แชร์ได้
เนื่องจากในกรณีนี้คุณกำลังจัดการกับตรรกะวงจรชีวิตของมุมมอง จึงควรใช้การสังเกตวงจร
บทสรุป
Dependency Injection เป็นเทคนิคที่ดีในการสร้างแอป Android ที่ปรับขนาดได้และทดสอบได้ ใช้คอนเทนเนอร์เพื่อแชร์อินสแตนซ์ของคลาสในส่วนต่างๆ ของแอป และเป็นศูนย์กลางในการสร้างอินสแตนซ์ของคลาสโดยใช้ Factory
เมื่อแอปพลิเคชันมีขนาดใหญ่ขึ้น คุณจะเริ่มเห็นว่าตัวเองเขียนโค้ดที่ซ้ำกันมาก (เช่น ฟีเจอร์) ซึ่งอาจทำให้เกิดข้อผิดพลาดได้ นอกจากนี้ คุณยังต้องจัดการขอบเขตและวงจรชีวิตของคอนเทนเนอร์ด้วยตนเองด้วย เพิ่มประสิทธิภาพและทิ้งคอนเทนเนอร์ที่ไม่จำเป็นแล้วเพื่อเพิ่มพื้นที่ว่างในหน่วยความจำ การทำอย่างไม่ถูกต้องอาจทำให้เกิดข้อบกพร่องเล็กๆ น้อยๆ และการรั่วไหลของหน่วยความจำในแอป
ในส่วน Dagger คุณจะได้เห็นวิธีใช้ Dagger เพื่อทำให้กระบวนการนี้ทำงานอัตโนมัติและสร้างโค้ดเดียวกันกับที่คุณเขียนด้วยตนเอง