การแทรกทรัพยากร Dependency ด้วยตนเอง

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

โดยปกติแล้วแอป Android จะประกอบไปด้วยคลาสหลายคลาส และบางคลาสจะขึ้นอยู่กับกัน
รูปที่ 1 โมเดลกราฟแอปพลิเคชันของแอป 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);
    }
}

แนวทางนี้มีปัญหาดังนี้

  1. มีโค้ดที่ต้องเขียนซ้ำจำนวนมาก หากต้องการสร้างอินสแตนซ์อื่นของ LoginViewModel ในส่วนอื่นของโค้ด คุณจะต้องเขียนโค้ดซ้ำ

  2. ต้องประกาศการพึ่งพาตามลําดับ คุณต้องสร้าง экземпляр UserRepository ก่อน LoginViewModel จึงจะสร้างได้

  3. การนำออบเจ็กต์มาใช้ซ้ำทำได้ยาก หากต้องการนํา 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();
    }
}

แนวทางนี้ดีกว่าแนวทางก่��นหน้า แต่ยังคงมีความท้าทายบางอย่างที่ควรพิจารณา ดังนี้

  1. คุณต้องจัดการ AppContainer ด้วยตนเอง โดยสร้างอินสแตนซ์สำหรับข้อกําหนดทั้งหมดด้วยตนเอง

  2. ยังมีโค้ดที่ต้องเขียนซ้ำจำนวนมาก คุณต้องสร้าง Factory หรือพารามิเตอร์ด้วยตนเอง โดยขึ้นอยู่กับว่าคุณต้องการใช้ออบเจ็กต์ซ้ำหรือไ��่

การจัดการทรัพยากร Dependency ในงานของแอปพลิเคชัน

AppContainer จะมีความซับซ้อนเมื่อคุณต้องการรวมฟังก์ชันการทำงานอื่นๆ ไว้ในโปรเจ็กต์ เมื่อแอปมีขนาดใหญ่ขึ้นและคุณเริ่มเปิดตัวขั้นตอนต่างๆ ของฟีเจอร์ ปัญหาที่อาจเกิดขึ้นก็จะยิ่งมากขึ้นไปอีก

  1. เมื่อคุณมีขั้นตอนที่แตกต่างกัน คุณอาจต้องการให้ออบเจ็กต์อยู่ในขอบเขตของขั้นตอนนั้นๆ เท่านั้น ตัวอย่างเช่น เมื่อสร้าง LoginUserData (ซึ่งอาจประกอบด้วยชื่อผู้ใช้และรหัสผ่านที่ใช้ในขั้นตอนการเข้าสู่ระบบเท่านั้น) คุณไม่ต้องการให้เก็บข้อมูลจากขั้นตอนการเข้าสู่ระบบเดิมของผู้ใช้รายอื่น คุณต้องมีอินสแตนซ์ใหม่สำหรับแต่ละขั้นตอนใหม่ ซึ่งทำได้โดยการสร้างออบเจ็กต์ FlowContainer ภายใน AppContainer ดังที่แสดงในตัวอย่างโค้ดถัดไป

  2. การเพิ่มประสิทธิภาพกราฟแอปพลิเคชันและคอนเทนเนอร์โฟลว์ก็อาจเป็นเรื่องยากเช่นกัน อย่าลืมลบอินสแตนซ์ที่ไม่จําเป็นออก ทั้งนี้ขึ้นอยู่กับขั้นตอนที่คุณทําอยู่

ลองจินตนาการว่าคุณมีขั้นตอนการเข้าสู่ระบบที่ประกอบด้วยกิจกรรม 1 รายการ (LoginActivity) และหลายส่วนที่แตกต่างออกไป (LoginUsernameFragment และ LoginPasswordFragment) มุมมองเหล่านี้ต้องการดำเนินการต่อไปนี้

  1. เข้าถึงอินสแตนซ์ LoginUserData เดียวกันที่ต้องแชร์จนกว่าขั้นตอนการเข้าสู่ระบบจะเสร็จสมบูรณ์

  2. สร้างอินสแตนซ์ใหม่ของ 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 เพื่อทำให้กระบวนการนี้ทำงานอัตโนมัติและสร้างโค้ดเดียวกันกับที่คุณเขียนด้วยตนเอง