การใช้ Dagger ในแอปที่มีหลายโมดูล

โปรเจ็กต์ที่มีโมดูล Gradle หลายรายการเรียกว่าโปรเจ็กต์ที่มีหลายโมดูล ในโปรเจ็กต์หลายโมดูลที่จัดส่งเป็น APK เดี่ยวโดยไม่มีฟีเจอร์ จึงเป็นเรื่องปกติที่จะมีโมดูล app ที่ขึ้นอยู่กับ โมดูลของโครงการ และโมดูล base หรือ core ที่ส่วนที่เหลือของ โมดูลมักจะขึ้นอยู่กับ โดยทั่วไปโมดูล app จะมี Application ส่วน base โมดูลมีชั้นเรียนทั่วไปทั้งหมดที่แชร์ในทุกโมดูลในโปรเจ็กต์ของคุณ

โดยโมดูล app เป็นตำแหน่งที่ดีในการประกาศคอมโพเนนต์ของแอปพลิเคชัน (สำหรับ เช่น ApplicationComponent ในรูปภาพด้านล่าง) ที่ระบุออบเจ็กต์ ที่คอมโพเนนต์อื่นๆ อาจต้องการ รวมถึงซิงเกิลตันของแอป ในฐานะ เช่น คลาสอย่าง OkHttpClient, โปรแกรมแยกวิเคราะห์ JSON, ตัวเข้าถึงฐานข้อมูล หรือ SharedPreferences ออบเจ็กต์ที่อาจกำหนดไว้ในโมดูล core จะจัดเตรียมโดย ApplicationComponent ที่กำหนดไว้ในโมดูล app

ในโมดูล app คุณยังมีคอมโพเนนต์อื่นๆ ที่มีอายุการใช้งานสั้นได้ด้วย ตัวอย่างเช่น UserComponent ที่มีการกำหนดค่าเฉพาะผู้ใช้ (เช่น UserSession) หลังจากเข้าสู่ระบบ

ในโมดูลต่างๆ ของโครงการ คุณสามารถระบุอย่างน้อยหนึ่งรายการ คอมโพเนนต์ย่อยที่มีตรรกะเฉพาะต่อโมดูลนั้นดังที่แสดงในรูปที่ 1

รูปที่ 1 ตัวอย่างกราฟ Dagger ใน โปรเจ็กต์หลายโมดูล

ตัวอย่างเช่น ในโมดูล login คุณอาจมี LoginComponent กำหนดขอบเขตโดยมีคำอธิบายประกอบ @ModuleScope ที่กำหนดเองซึ่งระบุออบเจ็กต์ที่พบได้ทั่วไป กับฟีเจอร์นั้น เช่น LoginRepository ในโมดูลนี้ คุณยังสามารถ มีคอมโพเนนต์อื่นๆ ที่ขึ้นอยู่กับ LoginComponent ที่มีแอตทริบิวต์ ตัวอย่างเช่น @FeatureScope สำหรับ LoginActivityComponent หรือ TermsAndConditionsComponent ที่กำหนดขอบเขตลอจิกเฉพาะฟีเจอร์ได้มากขึ้น เช่น ViewModel ออบเจ็กต์

สำหรับโมดูลอื่นๆ เช่น Registration คุณจะต้องตั้งค่าที่คล้ายกัน

กฎทั่วไปสำหรับโปรเจ็กต์หลายโมดูลคือโมดูลในระดับเดียวกัน ไม่ควรพึ่งพากัน หากมี ให้พิจารณาว่า (ทรัพยากร Dependency ระหว่าง 2 อย่างนี้) ควรเป็นส่วนหนึ่งของโมดูลระดับบน หากใช่ เปลี่ยนโครงสร้างภายในโค้ดเพื่อย้ายชั้นเรียนไปยังโมดูลระดับบนสุด หากไม่เป็นเช่นนั้น ให้สร้างโมดูลใหม่ ซึ่งขยายโมดูลหลักและมีโมดูลเดิมทั้ง 2 โมดูลขยายโมดูล โมดูลใหม่

ตามแนวทางปฏิบัติแนะนำ โดยทั่วไปคุณจะสร้างคอมโพเนนต์ใน ในกรณีต่อไปนี้

  • คุณต้องแทรกข้อมูลในช่องเช่นเดียวกับ LoginActivityComponent

  • คุณต้องกำหนดขอบเขตออบเจ็กต์ เช่นเดียวกับ LoginComponent

หากไม่มีกรณีเหล่านี้เลยและคุณต้องบอก Dagger ว่าจะให้ส่งอย่างไร จากโมดูลดังกล่าว ให้สร้างและแสดงโมดูล Dagger ด้วย @Provides หรือ @Binds เมธอด หากการแทรกการก่อสร้างสำหรับคลาสเหล่านั้นใช้ไม่ได้

การใช้งานด้วยคอมโพเนนต์ย่อย Dagger

หน้าเอกสารการใช้ Dagger ในแอป Android พูดถึงวิธีสร้างและใช้ คอมโพเนนต์ย่อย อย่างไรก็ตาม คุณไม่สามารถใช้รหัสเดียวกันได้ เนื่องจาก โมดูลฟีเจอร์ไม่ทราบเกี่ยวกับโมดูล app ตัวอย่างเช่น ถ้าคุณคิดว่า เกี่ยวกับขั้นตอนการเข้าสู่ระบบโดยทั่วไป และโค้ดที่เรามีในหน้าก่อนหน้านี้ คอมไพล์เพิ่มเติมอื่นๆ ดังนี้

Kotlin

class LoginActivity: Activity() {
  ...

  override fun onCreate(savedInstanceState: Bundle?) {
    // Creation of the login graph using the application graph
    loginComponent = (applicationContext as MyDaggerApplication)
                        .appComponent.loginComponent().create()

    // Make Dagger instantiate @Inject fields in LoginActivity
    loginComponent.inject(this)
    ...
  }
}

Java

public class LoginActivity extends Activity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Creation of the login graph using the application graph
        loginComponent = ((MyApplication) getApplicationContext())
                                .appComponent.loginComponent().create();

        // Make Dagger instantiate @Inject fields in LoginActivity
        loginComponent.inject(this);

        ...
    }
}

เนื่องจากโมดูล login ไม่ทราบเกี่ยวกับ MyApplication หรือ appComponent หากต้องการให้ใช้งานได้ คุณต้องกำหนดอินเทอร์เฟซในฟีเจอร์นี้ ที่ให้ FeatureComponent ซึ่ง MyApplication ต้องการ เพื่อนำไปปฏิบัติ

ในตัวอย่างต่อไปนี้ คุณจะกำหนดอินเทอร์เฟซ LoginComponentProvider ได้ ที่ระบุ LoginComponent ในโมดูล login สำหรับโฟลว์การเข้าสู่ระบบ

Kotlin

interface LoginComponentProvider {
    fun provideLoginComponent(): LoginComponent
}

Java

public interface LoginComponentProvider {
   public LoginComponent provideLoginComponent();
}

ตอนนี้ LoginActivity จะใช้อินเทอร์เฟซนั้นแทนข้อมูลโค้ด ตามที่ระบุไว้ข้างต้น

Kotlin

class LoginActivity: Activity() {
  ...

  override fun onCreate(savedInstanceState: Bundle?) {
    loginComponent = (applicationContext as LoginComponentProvider)
                        .provideLoginComponent()

    loginComponent.inject(this)
    ...
  }
}

Java

public class LoginActivity extends Activity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        loginComponent = ((LoginComponentProvider) getApplicationContext())
                                .provideLoginComponent();

        loginComponent.inject(this);

        ...
    }
}

ตอนนี้ MyApplication จะต้องนำอินเทอร์เฟซดังกล่าวมาใช้ วิธีการที่จำเป็น

Kotlin

class MyApplication: Application(), LoginComponentProvider {
  // Reference to the application graph that is used across the whole app
  val appComponent = DaggerApplicationComponent.create()

  override fun provideLoginComponent(): LoginComponent {
    return appComponent.loginComponent().create()
  }
}

Java

public class MyApplication extends Application implements LoginComponentProvider {
  // Reference to the application graph that is used across the whole app
  ApplicationComponent appComponent = DaggerApplicationComponent.create();

  @Override
  public LoginComponent provideLoginComponent() {
    return appComponent.loginComponent.create();
  }
}

นี่คือวิธีใช้คอมโพเนนต์ย่อย Dagger ในโปรเจ็กต์หลายโมดูล เมื่อใช้โมดูลฟีเจอร์ โซลูชันจะแตกต่างกันเพราะ จะขึ้นอยู่กับแต่ละแบบ

ทรัพยากร Dependency ของคอมโพเนนต์ที่มีโมดูลฟีเจอร์

ในโมดูลฟีเจอร์ โดยปกติแล้วโมดูลต่างๆ จะขึ้นอยู่กับ จะกลับด้านกัน แทนที่โมดูล app รวมถึงฟีเจอร์ โมดูล โมดูลฟีเจอร์จะขึ้นอยู่กับโมดูล app ดูรูปที่ 2 เพื่อดูการนำเสนอว่าโมดูลมีโครงสร้างอย่างไร

รูปที่ 2 ตัวอย่างกราฟ Dagger ใน โปรเจ็กต์ที่มีโมดูลฟีเจอร์

ใน Dagger คอมโพเนนต์จำเป็นต้องทราบเกี่ยวกับองค์ประกอบย่อยของตัวเอง ข้อมูลนี้ รวมอยู่ในโมดูล Dagger ที่เพิ่มในคอมโพเนนต์หลัก (เช่น SubcomponentsModule ในการใช้ Dagger ในแอป Android)

แต่ด้วยทรัพยากร Dependency แบบย้อนกลับระหว่างแอปและ โมดูลฟีเจอร์ คอมโพเนนต์ย่อยไม่ปรากฏจากโมดูล app เนื่องจาก ไม่ได้อยู่ในเส้นทางบิลด์ ตัวอย่างเช่น LoginComponent ที่กำหนดไว้ใน โมดูลฟีเจอร์ login ไม่สามารถเป็นองค์ประกอบย่อยของ ApplicationComponent ที่กำหนดไว้ในโมดูล app

Dagger มีกลไกที่เรียกว่าทรัพยากร Dependency ของคอมโพเนนต์ ซึ่งคุณใช้เพื่อ แก้ปัญหานี้ได้ แทนที่จะเป็นคอมโพเนนต์ย่อยของ คอมโพเนนต์หลัก คอมโพเนนต์ย่อยจะขึ้นอยู่กับคอมโพเนนต์หลัก ด้วย ว่าไม่มีความสัมพันธ์แบบแม่กับลูก ตอนนี้ คอมโพเนนต์จะขึ้นอยู่กับผู้ใช้รายอื่น เพื่อดูการขึ้นต่อกันบางอย่าง คอมโพเนนต์ต้องแสดงประเภทจากกราฟ สำหรับคอมโพเนนต์ที่เกี่ยวข้องเพื่อใช้งาน

เช่น โมดูลฟีเจอร์ชื่อ login ต้องการสร้าง LoginComponent ที่ขึ้นอยู่กับ AppComponent ที่มีใน app โมดูล Gradle

ด้านล่างนี้เป็นคำจำกัดความของชั้นเรียนและ AppComponent ที่เป็นส่วนหนึ่งของ โมดูล Gradle ของ app:

Kotlin

// UserRepository's dependencies
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }

// UserRepository is scoped to AppComponent
@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

@Singleton
@Component
interface AppComponent { ... }

Java

// UserRepository's dependencies
public class UserLocalDataSource {

    @Inject
    public UserLocalDataSource() {}
}

public class UserRemoteDataSource {

    @Inject
    public UserRemoteDataSource() { }
}

// UserRepository is scoped to AppComponent
@Singleton
public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

@Singleton
@Component
public interface ApplicationComponent { ... }

ในโมดูล Gradle ของ login ที่มีโมดูล Gradle app คุณมี LoginActivity ที่ต้องแทรกอินสแตนซ์ LoginViewModel:

Kotlin

// LoginViewModel depends on UserRepository that is scoped to AppComponent
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

// LoginViewModel depends on UserRepository that is scoped to AppComponent
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

LoginViewModel มีทรัพยากร Dependency ของ UserRepository ที่พร้อมใช้งานและ กำหนดขอบเขตเป็น AppComponent ลองสร้าง LoginComponent ที่ขึ้นอยู่กับ AppComponent เพื่อแทรก LoginActivity:

Kotlin

// Use the dependencies attribute in the Component annotation to specify the
// dependencies of this Component
@Component(dependencies = [AppComponent::class])
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

Java

// Use the dependencies attribute in the Component annotation to specify the
// dependencies of this Component
@Component(dependencies = AppComponent.class)
public interface LoginComponent {

    void inject(LoginActivity loginActivity);
}

LoginComponent ระบุทรัพยากร Dependency สำหรับ AppComponent ด้วยการเพิ่มทรัพยากรดังกล่าวลงใน พารามิเตอร์ Dependency ของคำอธิบายประกอบคอมโพเนนต์ เนื่องจาก LoginActivity จะ Dagger จะแทรกเมธอด ให้เพิ่มเมธอด inject() ลงในอินเทอร์เฟซ

เมื่อสร้าง LoginComponent อินสแตนซ์ของ AppComponent ต้องเป็น ผ่านเข้ามา โดยใช้โรงงานผลิตคอมโพเนนต์เพื่อดำเนินการ

Kotlin

@Component(dependencies = [AppComponent::class])
interface LoginComponent {

    @Component.Factory
    interface Factory {
        // Takes an instance of AppComponent when creating
        // an instance of LoginComponent
        fun create(appComponent: AppComponent): LoginComponent
    }

    fun inject(activity: LoginActivity)
}

Java

@Component(dependencies = AppComponent.class)
public interface LoginComponent {

    @Component.Factory
    interface Factory {
        // Takes an instance of AppComponent when creating
        // an instance of LoginComponent
        LoginComponent create(AppComponent appComponent);
    }

    void inject(LoginActivity loginActivity);
}

ตอนนี้ LoginActivity สามารถสร้างอินสแตนซ์ของ LoginComponent และเรียกเมธอด inject() วิธี

Kotlin

class LoginActivity: Activity() {

    // You want Dagger to provide an instance of LoginViewModel from the Login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Gets appComponent from MyApplication available in the base Gradle module
        val appComponent = (applicationContext as MyApplication).appComponent

        // Creates a new instance of LoginComponent
        // Injects the component to populate the @Inject fields
        DaggerLoginComponent.factory().create(appComponent).inject(this)

        super.onCreate(savedInstanceState)

        // Now you can access loginViewModel
    }
}

Java

public class LoginActivity extends Activity {

    // You want Dagger to provide an instance of LoginViewModel from the Login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Gets appComponent from MyApplication available in the base Gradle module
        AppComponent appComponent = ((MyApplication) getApplicationContext()).appComponent;

        // Creates a new instance of LoginComponent
        // Injects the component to populate the @Inject fields
        DaggerLoginComponent.factory().create(appComponent).inject(this);

        // Now you can access loginViewModel
    }
}

LoginViewModel ขึ้นอยู่กับ UserRepository และเพื่อให้ LoginComponent เข้าถึงได้จาก AppComponent โดย AppComponent ต้องเปิดเผยใน อินเทอร์เฟซ:

Kotlin

@Singleton
@Component
interface AppComponent {
    fun userRepository(): UserRepository
}

Java

@Singleton
@Component
public interface AppComponent {
    UserRepository userRepository();
}

กฎขอบเขตที่มีคอมโพเนนต์อ้างอิงทำงานในลักษณะเดียวกับ คอมโพเนนต์ย่อย เนื่องจาก LoginComponent ใช้อินสแตนซ์ของ AppComponent จะใช้คำอธิบายประกอบขอบเขตเดียวกันไม่ได้

หากต้องการกำหนดขอบเขตระดับ LoginViewModel เป็น LoginComponent คุณจะต้องทำในฐานะ ก่อนหน้านี้คุณใช้คำอธิบายประกอบ @ActivityScope ที่กำหนดเอง

Kotlin

@ActivityScope
@Component(dependencies = [AppComponent::class])
interface LoginComponent { ... }

@ActivityScope
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

@ActivityScope
@Component(dependencies = AppComponent.class)
public interface LoginComponent { ... }

@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

แนวทางปฏิบัติแนะนำ

  • ApplicationComponent ควรอยู่ในโมดูล app เสมอ

  • สร้างคอมโพเนนต์ Dagger ในโมดูลหากต้องการดำเนินการแทรกฟิลด์ ในโมดูลนี้ หรือคุณต้องกำหนดขอบเขตออบเจ็กต์สำหรับขั้นตอนที่เฉพาะเจาะจงของ แอปพลิเคชันของคุณ

  • สำหรับโมดูล Gradle ที่ใช้สำหรับยูทิลิตีหรือตัวช่วยต่างๆ และไม่จำเป็นต้องใช้ เพื่อสร้างกราฟ (คุณต้องมีส่วนประกอบของ Dagger) สร้างและแสดง โมดูล Dagger สาธารณะที่มีเมธอด @Provides และ @Binds ของคลาสเหล่านั้น ไม่สนับสนุนการแทรกตัวสร้าง

  • หากต้องการใช้ Dagger ในแอป Android ที่มีโมดูลฟีเจอร์ ให้ใช้คอมโพเนนต์ ของทรัพยากร Dependency เพื่อเข้าถึงทรัพยากร Dependency ที่ระบุโดย ApplicationComponent ที่กำหนดไว้ในโมดูล app