12/08/2018, 14:52

Service-Tree và Dagger2

Nếu bạn đã từng thử sử dụng Component dependencies (or subcomponents) của Dagger, bạn có thể gặp vấn đề sau: bạn tạo các phụ thuộc theo phạm vi cho của bạn Activity/Fragment  - nhưng khi bạn xoay màn hình, tất cả các phụ thuộc của bạn được tái tạo, bởi vì bạn tạo một component mới. Nếu bạn sử ...

Nếu bạn đã từng thử sử dụng Component dependencies (or subcomponents) của Dagger, bạn có thể gặp vấn đề sau: bạn tạo các phụ thuộc theo phạm vi cho của bạn Activity/Fragment  - nhưng khi bạn xoay màn hình, tất cả các phụ thuộc của bạn được tái tạo, bởi vì bạn tạo một component mới.

Nếu bạn sử dụng ứng dụng mẫu Ribot, BaseActivity của họ sẽ như sau:

public ActivityComponent activityComponent() {
    if(activityComponent == null) {
       activityComponent = DaggerActivityComponent.builder()
          .activityModule(new ActivityModule(this))
          .applicationComponent(
               RibotApplication.get(this).getComponent())
          .build();
    }
    return activityComponent;
}

Nhưng ngay sau đó bạn nhận ra rằng bạn cần phải lưu trữ các component trong một số loại bộ nhớ cache global. Nhưng nếu mỗi Activity và Fragment có component riêng của nó thì khi bạn hoàn thành Activity, bạn sẽ làm rõ các component của Fragments như thế nào? Như một phiền toái!

May mắn rằng, nếu bạn tạo một hierachical global cache, sẽ dễ hơn rất nhiều. Và service-trê là một cách để lưu trữ mọi thứ một cách có thứ bậc.

  1. Tạo một singleton component

Trước tiên, chúng ta cần một component singleton. Nó thường được tạo ra trong class CustomApplication, bởi vì nó được bảo đảm để tồn tại. ServiceTree của chúng ta cũng là một singleton, vì vậy nó có ý nghĩa đối với module cung cấp @Singleton của chúng ta method để tạo ra nó.

@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {
    ServiceTree serviceTree();
}
@Module
public class AppModule {
    @Provides
    @Singleton
    public ServiceTree serviceTree() {
        return new ServiceTree();
    }
}

Sau đó, chúng ta có thể tạo truy cập global này:

public class CustomApplication
        extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        AppComponent appComponent = DaggerAppComponent.create();
        Injector.INSTANCE.appComponent = appComponent;
        ServiceTree serviceTree = appComponent.serviceTree();
        serviceTree.registerRootService(
            Services.DAGGER_COMPONENT, appComponent);
    }
}

Great! Với điều đó, chúng ta đã tạo ra Service-Tree global, và tạo component gốc có thể truy cập tới mọi nút trong cây.

  1. Tạo Activity component

Bởi vì Fragments rất tricky, và sau khi quá trình chết, chúng được tái tạo bởi super.onCreate (savedInstanceState), chúng ta cần phải thiết lập component Activity trước khi gọi nó.

@Override
protected void onCreate(Bundle savedInstanceState) {
    serviceTree = Injector.get().serviceTree(); // <-- global service tree
    MainComponent mainComponent;
    if(!serviceTree.hasNodeWithKey(TAG)) {
        Binder binder = serviceTree.createRootNode(TAG);
        AppComponent appComponent = binder.getService(DAGGER_COMPONENT);
        mainComponent = DaggerMainComponent.builder()
                   .appComponent(appComponent)
                   .build();
        binder.bindService(DAGGER_COMPONENT, mainComponent);
    } else {
        mainComponent = serviceTree.getNode(TAG).getService(DAGGER_COMPONENT);
    }
    mainComponent.inject(this);
    super.onCreate(savedInstanceState);
  1. Tạo Fragment component

Không có gì ngạc nhiên khi các Fragment là những thứ kỳ quái nhất! Vòng đời của chúng là ít nhất có thể dự đoán được. Rõ ràng, chúng được tái tạo bởi super.onCreate của AppCompatActivity (), vì vậy sự recreate của chúng là khỏi tầm tay của chúng ta.

Callback đáng tin cậy duy nhất để bind một thứ gì đó tơi Fragment là trong onAttach(Context).

Đáng cười thay, điều này được gọi ngay cả khi Activity đã bị kill và tái tạo bởi hệ thống (configuration change), nhưng không có sự khác biệt giữa các sự kiện này. Cách duy nhất bạn có thể thấy đó là thay đổi cấu hình bằng cách kiểm tra fragment.getView () == null.

Tất nhiên, chúng ta không cần phải lo lắng về điều này ngay bây giờ, bởi vì chúng ta có thể sử dụng serviceTree.hasNodeWithKey (); Tất cả những gì chúng ta cần là tạo ra các service cho Fragments của chúng ta trong onAttach (Context).

public class FirstFragment extends Fragment implements HasServices {
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        MainActivity mainActivity = MainActivity.get(context);
        mainActivity.registerFragmentServices(this);
    }

Xem xét rằng chúng ta cần ràng buộc nó với pảent (và Activity cũng sẽ phải xử lý việc phá hủy ở các nút Fragment), chúng ta sẽ làm điều này trong Activity.

// in MainActivity
public void registerFragmentServices(Fragment fragment) {
    if(fragment != null && fragment instanceof HasServices) { 
        HasServices serviceFragment = ((HasServices) fragment);
        String newTag = serviceFragment.getNodeTag();
        if(!serviceTree.hasNodeWithKey(newTag)) {
            Binder binder = serviceTree.createChildNode(serviceTree.getNode(MainActivity.TAG), newTag);
            serviceFragment.bindServices(binder);
        }
    }
}
  1. Phá hủy singleton component

Về mặt kỹ thuật, xem xét rằng các thành phần singleton được ràng buộc với ứng dụng, và ứng dụng kéo dài miễn là quá trình ứng dụng tồn tại, không cần phải phá hủy component singleton. Đây là cái gì mà hệ thống có thể xử lý.

  1. Phá hủy Activity component

May mắn thay, có một cách đáng tin cậy để nói rằng Activity đang bị kill. Chúng ta có thể sử dụng onDestroy() callback với một check isFinishing().

@Override
protected void onDestroy() {
    super.onDestroy();
    if(isFinishing()) {
        serviceTree.removeNodeAndChildren(serviceTree.getNode(TAG));
    }
}
  1. Phá hủy Fragment component

Rõ ràng, nếu bạn sử dụng Fragment backtask, thì vòng đời của fragment trở nên kỳ lạ. Điều này sẽ không có gì ngạc nhiên ngay từ cái nhìn đầu tiên, nhưng chắc chắn vẫn còn có những điều bất ngờ. Nếu bạn chỉ cần đóng Activity, thì serviceTree.removeNodeAndChildren () cũng sẽ loại bỏ các dịch vụ Fragments khỏi cây. Tuy nhiên, nếu bạn di chuyển giữa các fragment bên trong Activity, và bạn đã thiết lập các fragment của mình như thường lệ ...

if(savedInstanceState == null) {
        getSupportFragmentManager()
              .beginTransaction()
              .add(R.id.root, new FirstFragment())
              .addToBackStack(null)
              .commit();
    }

Sau đó cách duy nhất bạn có thể tiếp tục theo dõi trong các active fragment là tin cậy vào getSupportFragmentManager().getFragments().

Đó là lí do nếu bạn làm các điều hướng điển hình

public void goToSecond() {
        getSupportFragmentManager()
              .beginTransaction()
              .replace(R.id.root, new SecondFragment())
              .addToBackStack(null)
              .commit();
    }

Sau đó FirstFragment sẽ bị remove, và không detach.

Vì vậy bạn không có cách nào đáng tin cậy để nói nếu FirstFragment vẫn còn tồn tại hay không, trừ khi yêu cầu FragmentManager gọi các active fragment, mà vẫn còn chứa nó.

Ngoài ra, để hỗ trợ back press, chúng ta ghi đè onBackPressed() như thường là cần thiết với các đoạn:

 @Override
    public void onBackPressed() {
        if(getSupportFragmentManager().getBackStackEntryCount() <= 1) {
            finish();
        } else {
            super.onBackPressed();
        }
    }

Và bây giờ chúng ta có hệ thống chuyển hướng thuận lợi và lạc hậu.

Hãy đảm bảo service của chúng ta bị phá hủy khi điều hướng lại! Đối với điều đó, chúng ta cần phải đăng ký một FragmentManager.OnBackStackChangedListener, vì đó là cách chúng ta có thể được thông báo rằng một fragment transaction đã xảy ra.

FragmentManager.OnBackStackChangedListener cho chúng ta một phương thức được gọi là onBackStackChanged (), nhưng nó không cung cấp cho chúng ta bất kỳ thông tin nào liên quan đến trạng thái trước hoặc trạng thái mới hiện tại, vì vậy chúng ta cần phải theo dõi trạng thái trước đó và yêu cầu các active fragment từ fragment manager để biết trạng thái mới.

Một sự thật buồn cười về fagment manager:

  • tại start-up, backstack listener được gọi, và active fragment list là NULL.
  • tại back navigation, active framents có size là 2, và phần tử thú 2 là NULL.

Với ý nghĩ đó, chúng ta sẽ setup nó như sau:

private List<String> activeTags = new ArrayList<>();

class BackstackListener
        implements FragmentManager.OnBackStackChangedListener {
    @Override
    public void onBackStackChanged() {
        List<String> newTags = collectActiveTags();
        for(String activeTag : activeTags) {
            if(!newTags.contains(activeTag)) {
                Log.d(TAG, "Destroying [" + activeTag + "]");
                serviceTree.removeNodeAndChildren(serviceTree.getNode(activeTag));
            }
        }
        activeTags = newTags;
    }
}

private List<String> collectActiveTags() {
    List<Fragment> fragments = getSupportFragmentManager().getFragments();
    if(fragments == null) {
        fragments = Collections.emptyList(); // active fragments is initially NULL instead of empty list
    }
    List<String> newTags = new LinkedList<>();
    for(Fragment fragment : fragments) {
        if(fragment != null && fragment instanceof HasServices) { // active fragments is a list that can have NULL element
            HasServices serviceFragment = ((HasServices) fragment);
            String newTag = serviceFragment.getNodeTag();
            newTags.add(newTag);
        }
    }
    return newTags;
}

Với điều đó, chúng ta có thể thu thập "new active tags" từ các fragment không phải null từ danh sách không phải là null và có thể cho biết nếu trạng thái trước đây chúng ta lưu trữ thủ công có chứa bất kỳ phần tử nào không còn trong trạng thái mới. Chúng ta tiêu diệt những nút đó, chúng không còn cần thiết.

Nhưng, vẫn còn một vấn đề nữa: các activeTags chúng ta đã thiết lập với việc thay thế ban đầu là KHÔNG khởi tạo vào cái process death! Có nghĩa là nếu bạn khởi động lại ứng dụng từ SecondFragment và bạn điều hướng trở lại, activeTags của bạn sẽ trống, và trạng thái mới sẽ là FirstFragment, NULL. Nghĩa là, service cho SecondFragment sẽ không bị phá hủy. Vì vậy, những gì bạn có thể làm là thiết lập các activeTags sau khi super.onCreate (), trong trường hợp nó chưa được khởi tạo.

super.onCreate(savedInstanceState); // <-- creates unattached but active fragments
if(activeTags.isEmpty() && getSupportFragmentManager().getFragments() != null) { 
    // handle process death
    activeTags = collectActiveTags();
}

Với điều đó, chúng ta có một thiết lập hoàn chỉnh cho các fragment trước sự thay đổi cấu hình, process death, chuyển tiếp và chuyển hướng ngược.

Hãy cùng nhìn lại toàn bộ:

public class MainActivity
        extends AppCompatActivity {
    private List<String> activeTags = new ArrayList<>();
        
    // ...

    class BackstackListener
            implements FragmentManager.OnBackStackChangedListener {
        @Override
        public void onBackStackChanged() {
            List<String> newTags = collectActiveTags();
            for(String activeTag : activeTags) {
                if(!newTags.contains(activeTag)) {
                    Log.d(TAG, "Destroying [" + activeTag + "]");
                    serviceTree.removeNodeAndChildren(serviceTree.getNode(activeTag));
                }
            }
            activeTags = newTags;
        }
    }

    private List<String> collectActiveTags() {
        List<Fragment> fragments = getSupportFragmentManager().getFragments();
        if(fragments == null) {
            fragments = Collections.emptyList(); // active fragments is initially NULL instead of empty list
        }
        List<String> newTags = new LinkedList<>();
        for(Fragment fragment : fragments) {
            if(fragment != null && fragment instanceof HasServices) { // active fragments is a list that can have NULL element
                HasServices serviceFragment = ((HasServices) fragment);
                String newTag = serviceFragment.getNodeTag();
                newTags.add(newTag);
            }
        }
        return newTags;
    }

    public void registerFragmentServices(Fragment fragment) {
        if(fragment != null && fragment instanceof HasServices) {
            HasServices serviceFragment = ((HasServices) fragment);
            String newTag = serviceFragment.getNodeTag();
            if(!serviceTree.hasNodeWithKey(newTag)) {
                serviceFragment.bindServices(serviceTree.createChildNode(serviceTree.getNode(TAG), newTag));
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //...
        getSupportFragmentManager().addOnBackStackChangedListener(new BackstackListener());

        super.onCreate(savedInstanceState);
        if(activeTags.isEmpty() && getSupportFragmentManager().getFragments() != null) { 
            // handle process death
            activeTags = collectActiveTags();
        }

        //...
            
        if(savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction().add(R.id.root, new FirstFragment()).addToBackStack(null).commit();
        }
    }

    @Override
    public void onBackPressed() {
        if(getSupportFragmentManager().getBackStackEntryCount() <= 1) {
            finish();
        } else {
            super.onBackPressed();
        }
    }

    public void goToSecond() {
        getSupportFragmentManager().beginTransaction().replace(R.id.root, new SecondFragment()).addToBackStack(null).commit();
    }

    //...
}

Bây giờ nếu chỉ:

  • Sự tái tạo của các yếu tố ứng dụng của chúng ta đã được dự đoán.
  • Được thông báo về những thay đổi trong trạng thái đăng ký là đáng tin cậy
  • Chúng ta đã không phải lưu trữ rõ ràng trạng thái trước đó.

Tin tốt là, chúng ta thực sự có thể làm điều đó! Chúng ta chỉ cần phải bỏ phần fragment backstack. (Và các fragment)

Chúng ta có thể thay thế BackstackChangeListener như sau:

public class ServiceManager {
    public void setupServices(StateChange stateChange) {
        for(Object _previousKey : stateChange.getPreviousState()) {
            Key previousKey = (Key) _previousKey;
            if(!stateChange.getNewState().contains(previousKey)) {
                ServiceTree.Node previousNode = serviceTree.getNode(previousKey);
                serviceTree.removeNodeAndChildren(previousNode);
            }
        }
        for(Object _newKey : stateChange.getNewState()) {
            Key newKey = (Key) _newKey;
            if(!serviceTree.hasNodeWithKey(newKey)) {
                ServiceTree.Node.Binder binder = serviceTree.createRootNode(newKey);
                newKey.bindServices(binder);
            }
        }
        // end services
    }

Và Activity sẽ như sau:

public class MainActivity
        extends AppCompatActivity
        implements StateChanger {
    @BindView(R.id.root)
    RelativeLayout root;

    BackstackDelegate backstackDelegate;
        
    ServiceManager serviceManager;
    
    ServiceTree serviceTree;
    
    public void goToSecond() {
        backstackDelegate.getBackstack().goTo(new SecondKey());
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        serviceTree = Injector.get().serviceTree();
        serviceManager = new ServiceManager(serviceTree);
        // ... setup activity component here
        backstackDelegate = new BackstackDelegate(null);
        backstackDelegate.onCreate(savedInstanceState, //
                getLastCustomNonConfigurationInstance(), //
                HistoryBuilder.single(new FirstKey()));
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        
        backstackDelegate.setStateChanger(this);
    }
    
    @Override
    public void onBackPressed() {
        if(!backstackDelegate.onBackPressed()) {
            super.onBackPressed();
        }
    }
    
    @Override
    public void handleStateChange(StateChange stateChange, Callback completionCallback) {
        serviceManager.setupServices(stateChange);
        if(stateChange.topNewState().equals
            
            
            
         
0