Dagger- Chúng ta có nên tạo từng thành phần và mô-đun cho từng Hoạt động / Phân mảnh không


85

Tôi đã làm việc với dagger2 một thời gian. Và tôi đã bối rối hơn khi tạo một thành phần / mô-đun riêng cho mỗi Hoạt động / Phân mảnh. Vui lòng giúp tôi làm rõ điều này:

Ví dụ: Chúng tôi có một ứng dụng và ứng dụng có khoảng 50 màn hình. Chúng tôi sẽ triển khai mã theo sau mẫu MVP và Dagger2 cho DI. Giả sử rằng chúng ta có 50 hoạt động và 50 người thuyết trình.

Theo ý kiến ​​của tôi, thông thường chúng ta nên tổ chức mã như thế này:

  1. Tạo một AppComponent và AppModule sẽ cung cấp tất cả các đối tượng sẽ được sử dụng khi ứng dụng đang mở.

    @Module
    public class AppModule {
    
        private final MyApplicationClass application;
    
        public AppModule(MyApplicationClass application) {
            this.application = application;
        }
    
        @Provides
        @Singleton
        Context provideApplicationContext() {
            return this.application;
        }
    
        //... and many other providers 
    
    }
    
    @Singleton
    @Component( modules = { AppModule.class } )
    public interface AppComponent {
    
        Context getAppContext();
    
        Activity1Component plus(Activity1Module module);
        Activity2Component plus(Activity2Module module);
    
        //... plus 48 methods for 48 other activities. Suppose that we don't have any other Scope (like UserScope after user login, ....)
    
    }
    
  2. Tạo ActivityScope:

    @Scope
    @Documented
    @Retention(value=RUNTIME)
    public @interface ActivityScope {
    }
    
  3. Tạo Thành phần và Mô-đun cho mỗi Hoạt động. Thông thường tôi sẽ đặt chúng dưới dạng các lớp tĩnh trong lớp Hoạt động:

    @Module
    public class Activity1Module {
    
        public LoginModule() {
        }
        @Provides
        @ActivityScope
        Activity1Presenter provideActivity1Presenter(Context context, /*...some other params*/){
            return new Activity1PresenterImpl(context, /*...some other params*/);
        }
    
    }
    
    @ActivityScope
    @Subcomponent( modules = { Activity1Module.class } )
    public interface Activity1Component {
        void inject(Activity1 activity); // inject Presenter to the Activity
    }
    
    // .... Same with 49 remaining modules and components.
    

Đó chỉ là những ví dụ rất đơn giản để cho thấy cách tôi thực hiện điều này.

Nhưng một người bạn của tôi vừa đưa cho tôi một cách triển khai khác:

  1. Tạo PresenterModule sẽ cung cấp cho tất cả những người thuyết trình:

    @Module
    public class AppPresenterModule {
    
        @Provides
        Activity1Presenter provideActivity1Presentor(Context context, /*...some other params*/){
            return new Activity1PresenterImpl(context, /*...some other params*/);
        }
    
        @Provides
        Activity2Presenter provideActivity2Presentor(Context context, /*...some other params*/){
            return new Activity2PresenterImpl(context, /*...some other params*/);
        }
    
        //... same with 48 other presenters.
    
    }
    
  2. Tạo AppModule và AppComponent:

    @Module
    public class AppModule {
    
        private final MyApplicationClass application;
    
        public AppModule(MyApplicationClass application) {
            this.application = application;
        }
    
        @Provides
        @Singleton
        Context provideApplicationContext() {
            return this.application;
        }
    
        //... and many other provides 
    
    }
    
    @Singleton
    @Component(
            modules = { AppModule.class,  AppPresenterModule.class }
    )
    public interface AppComponent {
    
        Context getAppContext();
    
        public void inject(Activity1 activity);
        public void inject(Activity2 activity);
    
        //... and 48 other methods for 48 other activities. Suppose that we don't have any other Scope (like UserScope after user login, ....)
    
    }
    

Giải thích của anh ấy là: Anh ấy không phải tạo các thành phần và mô-đun cho mỗi hoạt động. Tôi nghĩ ý kiến ​​của bạn bè tôi hoàn toàn không tốt chút nào, nhưng xin hãy sửa cho tôi nếu tôi sai. Đây là những lý do:

  1. Rò rỉ nhiều bộ nhớ :

    • Ứng dụng sẽ tạo ra 50 người thuyết trình ngay cả khi người dùng chỉ mở 2 Hoạt động.
    • Sau khi người dùng đóng một Hoạt động, người trình bày của Hoạt động đó sẽ vẫn
  2. Điều gì xảy ra nếu tôi muốn tạo hai phiên bản của một Hoạt động? (làm thế nào anh ta có thể tạo ra hai người thuyết trình)

  3. Sẽ mất rất nhiều thời gian để khởi tạo ứng dụng (vì phải tạo nhiều người trình bày, đối tượng, ...)

Xin lỗi vì một bài dài, nhưng hãy giúp tôi làm rõ điều này cho tôi và bạn tôi, tôi không thể thuyết phục anh ta. Ý kiến ​​của bạn sẽ được đánh giá rất cao.

/ ------------------------------------------------- ---------------------- /

Chỉnh sửa sau khi làm bản demo.

Đầu tiên, cảm ơn câu trả lời của @pandawarrior. Tôi nên tạo một Demo trước khi tôi hỏi câu hỏi này. Tôi hy vọng kết luận của tôi ở đây có thể giúp ích cho người khác.

  1. Những gì bạn tôi đã làm không gây ra rò rỉ bộ nhớ trừ khi anh ấy đặt bất kỳ Phạm vi nào cho các phương thức Cung cấp. (Ví dụ: @Singleton hoặc @UserScope, ...)
  2. Chúng tôi có thể tạo nhiều người trình bày, nếu phương thức Cung cấp không có bất kỳ Phạm vi nào. (Vì vậy, điểm thứ hai của tôi cũng sai)
  3. Dagger sẽ chỉ tạo người trình bày khi họ cần. (Vì vậy, ứng dụng sẽ không mất nhiều thời gian để khởi tạo, tôi đã nhầm lẫn với Lazy Injection)

Vì vậy, tất cả những lý do tôi đã nói ở trên hầu hết là sai. Nhưng nó không có nghĩa là chúng ta nên làm theo ý tưởng của bạn tôi, vì hai lý do:

  1. Nó không tốt cho kiến ​​trúc của nguồn, khi anh ta loại bỏ tất cả những người trình bày trong mô-đun / thành phần. (Nó vi phạm nguyên tắc phân tách Giao diện , có thể cả nguyên tắc Trách nhiệm đơn lẻ ).

  2. Khi chúng tôi tạo một Thành phần phạm vi, chúng tôi sẽ biết khi nào nó được tạo và khi nào nó bị phá hủy, đó là một lợi ích rất lớn để tránh rò rỉ bộ nhớ. Vì vậy, đối với mỗi Hoạt động, chúng ta nên tạo một Thành phần bằng @ActivityScope. Hãy tưởng tượng, với việc triển khai bạn bè của tôi, mà chúng tôi quên đặt một số Phạm vi trong phương thức Nhà cung cấp => rò rỉ bộ nhớ sẽ xảy ra.

Theo tôi, với một ứng dụng nhỏ (chỉ một vài màn hình mà không có nhiều phần phụ thuộc hoặc có các phần phụ thuộc tương tự), chúng tôi có thể áp dụng ý tưởng của bạn bè tôi, nhưng tất nhiên là không nên.

Thích đọc thêm về: Điều gì quyết định vòng đời của một thành phần (đồ thị đối tượng) trong Dagger 2? Phạm vi hoạt động của Dagger2, tôi cần bao nhiêu mô-đun / thành phần?

Và một lưu ý nữa: Nếu bạn muốn xem khi nào đối tượng bị hủy, bạn có thể gọi các phương thức đó cùng nhau và GC sẽ chạy ngay lập tức:

    System.runFinalization();
    System.gc();

Nếu bạn chỉ sử dụng một trong những phương pháp này, GC sẽ chạy muộn hơn và bạn có thể nhận được kết quả sai.

Câu trả lời:


85

Khai báo một mô-đun riêng biệt cho mỗi mô-đun Activitykhông phải là một ý tưởng hay. Khai báo thành phần riêng biệt cho từng thành phần Activitythậm chí còn tệ hơn. Lý do đằng sau điều này rất đơn giản - bạn không thực sự cần tất cả các mô-đun / thành phần này (như bạn đã thấy).

Tuy nhiên, chỉ có một thành phần gắn liền với Applicationvòng đời của nó và sử dụng nó để tiêm vào tất cả Activitiescũng không phải là giải pháp tối ưu (đây là cách tiếp cận của bạn bè bạn). Nó không phải là tối ưu vì:

  1. Nó giới hạn bạn chỉ trong một phạm vi ( @Singletonhoặc một phạm vi tùy chỉnh)
  2. Phạm vi duy nhất bạn bị hạn chế làm cho các đối tượng được đưa vào là "ứng dụng đơn", do đó, sai lầm trong việc xác định phạm vi hoặc sử dụng không chính xác các đối tượng trong phạm vi có thể dễ dàng gây ra rò rỉ bộ nhớ chung
  3. Bạn cũng sẽ muốn sử dụng Dagger2 để đưa vào Services, nhưng Servicescó thể yêu cầu các đối tượng khác với Activities(ví dụ: Serviceskhông cần người thuyết trình, không có FragmentManager, v.v.). Bằng cách sử dụng một thành phần duy nhất, bạn mất tính linh hoạt trong việc xác định các biểu đồ đối tượng khác nhau cho các thành phần khác nhau.

Vì vậy, một thành phần mỗi Activitylà một thành phần quá mức cần thiết, nhưng một thành phần cho toàn bộ ứng dụng không đủ linh hoạt. Giải pháp tối ưu là ở giữa những cực trị này (như nó thường xảy ra).

Tôi sử dụng cách tiếp cận sau:

  1. Thành phần "ứng dụng" đơn cung cấp các đối tượng "toàn cục" (ví dụ: các đối tượng giữ trạng thái toàn cục được chia sẻ giữa tất cả các thành phần trong ứng dụng). Khởi tạo trong Application.
  2. Thành phần con "bộ điều khiển" của thành phần "ứng dụng" cung cấp các đối tượng được yêu cầu bởi tất cả các "bộ điều khiển" hướng tới người dùng (trong kiến ​​trúc của tôi là ActivitiesFragments). Khởi tạo trong mỗi ActivityFragment.
  3. Thành phần con "dịch vụ" của thành phần "ứng dụng" cung cấp các đối tượng được yêu cầu bởi tất cả Services. Lập tức trong mỗi Service.

Sau đây là một ví dụ về cách bạn có thể thực hiện cùng một phương pháp.


Chỉnh sửa tháng 7 năm 2017

Tôi đã xuất bản một video hướng dẫn cho thấy cách cấu trúc mã tiêm phụ thuộc Dagger trong ứng dụng Android : Hướng dẫn sử dụng Android Dagger cho Chuyên gia .


Chỉnh sửa tháng 2 năm 2018

Tôi đã xuất bản một khóa học hoàn chỉnh về tiêm phụ thuộc trong Android .

Trong khóa học này, tôi giải thích lý thuyết về tiêm phụ thuộc và chỉ ra cách nó xuất hiện tự nhiên trong ứng dụng Android. Sau đó, tôi chứng minh cách các cấu trúc Dagger phù hợp với sơ đồ tiêm phụ thuộc chung.

Nếu bạn tham gia khóa học này, bạn sẽ hiểu tại sao ý tưởng có một định nghĩa riêng biệt về mô-đun / thành phần cho mỗi Hoạt động / Phân mảnh về cơ bản là thiếu sót theo cách cơ bản nhất.

Cách tiếp cận như vậy làm cho cấu trúc của lớp trình bày từ tập hợp các lớp "Chức năng" được sao chép thành cấu trúc của tập hợp các lớp "Xây dựng", do đó ghép chúng lại với nhau. Điều này đi ngược lại mục tiêu chính của việc tiêm phụ thuộc là giữ cho các tập hợp lớp "Xây dựng" và "Chức năng" rời rạc.


Phạm vi ứng dụng:

@ApplicationScope
@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {

    // Each subcomponent can depend on more than one module
    ControllerComponent newControllerComponent(ControllerModule module);
    ServiceComponent newServiceComponent(ServiceModule module);

}


@Module
public class ApplicationModule {

    private final Application mApplication;

    public ApplicationModule(Application application) {
        mApplication = application;
    }

    @Provides
    @ApplicationScope
    Application applicationContext() {
        return mApplication;
    }

    @Provides
    @ApplicationScope
    SharedPreferences sharedPreferences() {
        return mApplication.getSharedPreferences(Constants.PREFERENCES_FILE, Context.MODE_PRIVATE);
    }

    @Provides
    @ApplicationScope
    SettingsManager settingsManager(SharedPreferences sharedPreferences) {
        return new SettingsManager(sharedPreferences);
    }
}

Phạm vi điều khiển:

@ControllerScope
@Subcomponent(modules = {ControllerModule.class})
public interface ControllerComponent {

    void inject(CustomActivity customActivity); // add more activities if needed

    void inject(CustomFragment customFragment); // add more fragments if needed

    void inject(CustomDialogFragment customDialogFragment); // add more dialogs if needed

}



@Module
public class ControllerModule {

    private Activity mActivity;
    private FragmentManager mFragmentManager;

    public ControllerModule(Activity activity, FragmentManager fragmentManager) {
        mActivity = activity;
        mFragmentManager = fragmentManager;
    }

    @Provides
    @ControllerScope
    Context context() {
        return mActivity;
    }

    @Provides
    @ControllerScope
    Activity activity() {
        return mActivity;
    }

    @Provides
    @ControllerScope
    DialogsManager dialogsManager(FragmentManager fragmentManager) {
        return new DialogsManager(fragmentManager);
    }

    // @Provides for presenters can be declared here, or in a standalone PresentersModule (which is better)
}

Và sau đó trong Activity:

public class CustomActivity extends AppCompatActivity {

    @Inject DialogsManager mDialogsManager;

    private ControllerComponent mControllerComponent;

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

    }

    private ControllerComponent getControllerComponent() {
        if (mControllerComponent == null) {

            mControllerComponent = ((MyApplication)getApplication()).getApplicationComponent()
                    .newControllerComponent(new ControllerModule(this, getSupportFragmentManager()));
        }

        return mControllerComponent;
    }
}

Thông tin bổ sung về tiêm phụ thuộc:

Dagger 2 Scopes Demystified

Dependency Injection trong Android


1
Cảm ơn @vasiliy đã chia sẻ ý kiến ​​của bạn. Đây chính xác là cách tôi sẽ sử dụng nó và hiện đang theo chiến lược. Trong trường hợp có mẫu MVP, người được giới thiệu ControllerModulesẽ tạo một mẫu mới Presentervà sau đó người trình bày được đưa vào dấu Activityhoặc Fragment. Bất kỳ ý kiến ​​vững chắc ủng hộ hoặc chống lại điều này?
Wahib Ul Haq

@Vasiliy, tôi đã đọc toàn bộ bài viết của bạn và tôi thấy rằng có thể bạn không xem xét người tương tác và người trình bày trong cơ chế. ControllerModule có cung cấp tất cả sự phụ thuộc của người tương tác và người trình bày không? Hãy đưa ra một gợi ý nhỏ trong trường hợp tôi bỏ sót bất cứ điều gì.
iamcrypticcoder

@ mahbub.kuet, nếu tôi hiểu những gì bạn đang đề cập đến bởi "người tương tác" và "người trình bày", ControllerComponentthì nên đưa họ vào. Cho dù bạn gắn chúng vào bên trong ControllerModulehay giới thiệu mô-đun bổ sung là tùy thuộc vào bạn. Trong các ứng dụng thực, tôi khuyên bạn nên sử dụng phương pháp tiếp cận đa mô-đun cho mỗi thành phần thay vì đặt mọi thứ vào một mô-đun duy nhất. Đây là một ví dụ cho ApplicationComponent, nhưng bộ điều khiển sẽ giống nhau: github.com/techyourchance/idocare-android/tree/master/app/src/…
Vasiliy

2
@ Mr.Hyde, nói chung là có, nhưng sau đó bạn sẽ phải khai báo rõ ràng trong ApplicationComponenttất cả các phụ thuộc ControllerComponentcó thể sử dụng. Ngoài ra, số phương thức của mã được tạo sẽ cao hơn. Tôi vẫn chưa tìm thấy lý do chính đáng để sử dụng các thành phần phụ thuộc.
Vasiliy

1
Tôi đang sử dụng cách tiếp cận này trong tất cả các dự án của mình ngày hôm nay và tôi rõ ràng không sử dụng bất kỳ thứ gì từ dagger.androidgói vì tôi thấy nó không có động cơ. Do đó, ví dụ này vẫn còn rất nhiều cập nhật và vẫn là cách tốt nhất để thực hiện DI trong Android IMHO.
Vasiliy

15

Bạn có thể tìm thấy một số ví dụ hay nhất về cách tổ chức các thành phần, mô-đun và gói của mình trong repo Google Android Architecture Blueprints Github tại đây .

Nếu bạn kiểm tra mã nguồn ở đó, bạn có thể thấy có một Thành phần phạm vi ứng dụng duy nhất (với vòng đời của toàn bộ ứng dụng) và sau đó tách Thành phần phạm vi hoạt động cho Hoạt động và Phân đoạn tương ứng với một chức năng nhất định trong dự án. Ví dụ, có các gói sau:

addedittask
taskdetail
tasks

Bên trong mỗi gói có một mô-đun, thành phần, người trình bày, v.v. Ví dụ, bên trong taskdetailcó các lớp sau:

TaskDetailActivity.java
TaskDetailComponent.java
TaskDetailContract.java
TaskDetailFragment.java
TaskDetailPresenter.java
TaskDetailPresenterModule.java

Lợi thế của việc tổ chức theo cách này (thay vì nhóm tất cả các hoạt động trong một thành phần hoặc mô-đun) là bạn có thể tận dụng các công cụ sửa đổi trợ năng của Java và hoàn thành mục Java hiệu quả 13. Nói cách khác, các lớp được nhóm theo chức năng sẽ giống nhau gói và bạn có thể tận dụng protectedvà các công cụ package-private sửa đổi trợ năng để ngăn chặn việc sử dụng không mong muốn đối với các lớp của bạn.


1
đây cũng là cách tiếp cận ưa thích của tôi. Tôi không thích các hoạt động / phân đoạn có quyền truy cập vào những thứ mà chúng không được phép.
Joao Sousa

3

Tùy chọn đầu tiên tạo một thành phần được lồng vào cho mỗi hoạt động, trong đó hoạt động có thể tạo các thành phần được lồng vào chỉ cung cấp sự phụ thuộc (người trình bày) cho hoạt động cụ thể đó.

Tùy chọn thứ hai tạo ra một @Singletonthành phần duy nhất có thể cung cấp cho người trình bày dưới dạng phụ thuộc chưa được giám sát, có nghĩa là khi bạn truy cập chúng, bạn tạo một phiên bản mới của người trình bày mỗi lần. (Không, nó không tạo một phiên bản mới cho đến khi bạn yêu cầu).


Về mặt kỹ thuật, không có phương pháp nào tệ hơn phương pháp kia. Cách tiếp cận đầu tiên không phân tách người thuyết trình theo tính năng, mà theo lớp.

Tôi đã sử dụng cả hai, chúng đều hoạt động và cả hai đều có ý nghĩa.

Nhược điểm duy nhất của giải pháp đầu tiên (nếu bạn đang sử dụng @Component(dependencies={...}thay vì @Subcomponent) là bạn cần đảm bảo rằng đó không phải là Activity tạo mô-đun của chính nó trong nội bộ, vì khi đó bạn không thể thay thế các triển khai phương thức mô-đun bằng mocks. Sau đó, một lần nữa, nếu bạn sử dụng phương thức chèn vào phương thức khởi tạo thay vì chèn trường, bạn có thể tạo lớp trực tiếp với phương thức khởi tạo, trực tiếp tạo cho nó những trò đùa.


1

Sử dụng Provider<"your component's name">thay vì thực hiện các thành phần đơn giản để tránh rò rỉ bộ nhớ và tạo ra hàng tấn thành phần vô dụng. Do đó, các thành phần của bạn sẽ được tạo bởi lazy khi bạn gọi phương thức get () vì bạn không cung cấp một thể hiện của thành phần mà thay vào đó chỉ cung cấp trình cung cấp. Do đó, người trình bày của bạn sẽ được áp dụng nếu .get () của nhà cung cấp được gọi. Đọc về Nhà cung cấp tại đây và áp dụng điều này. ( Tài liệu chính thức của Dagger )


Và một cách tuyệt vời khác là sử dụng multibinding. Phù hợp với nó, bạn nên ràng buộc những người thuyết trình của mình vào bản đồ và tạo họ thông qua các nhà cung cấp khi bạn cần. ( đây là tài liệu về đa liên kết )


-5

Bạn của bạn nói đúng, bạn không thực sự phải tạo các thành phần và mô-đun cho mọi hoạt động. Dagger được cho là sẽ giúp bạn giảm thiểu các đoạn mã lộn xộn và làm cho các hoạt động Android của bạn trở nên sạch sẽ hơn bằng cách ủy quyền các khởi tạo lớp cho Mô-đun thay vì khởi tạo chúng trong phương thức onCreate của Hoạt động.

Thông thường chúng tôi sẽ làm như thế này

public class MainActivity extends AppCompatActivity {


Presenter1 mPresenter1;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mPresenter1 = new Presenter1(); // you instantiate mPresentation1 in onCreate, imagine if there are 5, 10, 20... of objects for you to instantiate.
}

}

Bạn làm điều này thay thế

public class MainActivity extends AppCompatActivity {

@Inject
Presenter1 mPresenter1; // the Dagger module take cares of instantiation for your

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    injectThisActivity();
}

private void injectThisActivity() {
    MainApplication.get(this)
            .getMainComponent()
            .inject(this);
}}

Vì vậy, viết quá nhiều thứ có thể đánh bại mục đích của dao găm không? Tôi thích tạo trình bày của mình trong Hoạt động nếu tôi phải tạo Mô-đun và Thành phần cho mọi Hoạt động.

Đối với câu hỏi của bạn về:

1- Rò rỉ bộ nhớ:

Không, không trừ khi bạn đặt @Singletonchú thích cho những người thuyết trình mà bạn cung cấp. Dagger sẽ chỉ tạo đối tượng bất cứ khi nào bạn thực hiện một @Injecttrong lớp đích`. Nó sẽ không tạo ra những người thuyết trình khác trong kịch bản của bạn. Bạn có thể thử sử dụng Nhật ký để xem chúng có được tạo hay không.

@Module
public class AppPresenterModule {

@Provides
@Singleton // <-- this will persists throughout the application, too many of these is not good
Activity1Presenter provideActivity1Presentor(Context context, ...some other params){
    Log.d("Activity1Presenter", "Activity1Presenter initiated");
    return new Activity1PresenterImpl(context, ...some other params);
}

@Provides // Activity2Presenter will be provided every time you @Inject into the activity
Activity2Presenter provideActivity2Presentor(Context context, ...some other params){
    Log.d("Activity2Presenter", "Activity2Presenter initiated");
    return new Activity2PresenterImpl(context, ...some other params);
}

.... Same with 48 others presenters.

}

2- Bạn tiêm hai lần và ghi mã băm của họ

//MainActivity.java
@Inject Activity1Presenter mPresentation1
@Inject Activity1Presenter mPresentation2

@Inject Activity2Presenter mPresentation3
@Inject Activity2Presenter mPresentation4
//log will show Presentation2 being initiated twice

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    injectThisActivity();
    Log.d("Activity1Presenter1", mPresentation1.hashCode());
    Log.d("Activity1Presenter2", mPresentation2.hashCode());
    //it will shows that both have same hash, it's a Singleton
    Log.d("Activity2Presenter1", mPresentation3.hashCode());
    Log.d("Activity2Presenter2", mPresentation4.hashCode());
    //it will shows that both have different hash, hence different objects

3. Không, các đối tượng sẽ chỉ được tạo khi bạn @Injecttham gia vào các hoạt động, thay vì init ứng dụng.


1
Cảm ơn bạn đã nhận xét, những gì bạn đã nói không sai, nhưng tôi nghĩ đó không phải là câu trả lời tốt nhất, hãy xem bài chỉnh sửa của tôi. Vì vậy, không thể đánh dấu nó là được chấp nhận.
Mr Mike

@EpicPandaForce: Ơ, nhưng bạn phải khởi tạo nó ở đâu đó. Một cái gì đó sẽ phải vi phạm nguyên tắc nghịch đảo phụ thuộc.
David Liu
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.