Android component dependency detection

This is not another article about Dagger and its features. There will not be a word about other DI frameworks.

scan

The purpose of this publication is to demonstrate an approach to obtaining dependencies in fragments, dialogs, and activity.

Setting a listener for dialogue


In one of the inherited projects, I came across the following implementation of the dialogue:

public class ExampleDialogFragment extends DialogFragment {
    private Listener listener;
    public interface Listener {
        void onMessageEntered(String msg);
    }
    @Override
    public void onAttach (Context context) {
        super.onAttach(context);
        if(context instanceOf Listener) {
            listener = (Listener) context;
        } else {
            listener = (Listener) getParentFragment();
        }
    }
}

“Since when should a component search for a listener,” I thought at that moment.
Let's make it so that the fragment does not know who exactly implements the listener interface.
Many can immediately offer, for example, this option:

public class ExampleDialogFragment extends DialogFragment {
    private Listener listener;
    public interface Listener {
        void onMessageEntered(String msg);
    }
    public static DialogFragment newInstance(Listener listener) {
        ExampleDialogFragment dialogFragment = new ExampleDialogFragment();
        dialogFragment.listener = listener;
        return dialogFragment;
    }
}

and the activation code in which we will embed this dialog:

public class ExampleActivity extends AppCompatActivity {
    void showDialog() { 
        DialogFragment dialogFragment = ExampleDialogFragment
            .newInstance(new DialogFragment.Listener() {
                @Override
                void onMessageEntered(String msg) {
                    // TODO 
                }
        }); 
        dialogFragment.show(getFragmentManager(), "dialog");
    }
}

This solution has one significant drawback. When changing the configuration (for example, flipping the screen) we get the following chain: the dialogue will save its state in Bundleand be destroyed -> the activity will be deleted -> a new instance of the activity will be created -> the dialogue will be created again based on the saved Bundlestate. As a result, we will lose the link to the listener in the dialogue, since it obviously has not been saved and restored. setListener()Of course, we can manually call activity in one of the callbacks of the life cycle, but there is another option. Since we cannot store an anonymous class in Bundle, as well as instances of ordinary classes, we need to observe the following conditions:

  1. The listener must implement an interface SerializableorParcelable
  2. Pass listener through fragment arguments when creating it setArguments(Bundle args)
  3. The listener must be stored in the method onSaveInstanceState(Bundle outState)
  4. The listener must be restored in the method Dialog onCreateDialog(Bundle savedInstanceState)

As a rule, the listener interface is implemented by such Android components as Activityor Fragment. Such components are not intended to be stored in Bundle, so we need to find a different approach to the solution. Let’s try to convey not the listener himself, but the Provider” who is able to find him. In this case, no one bothers us to make it serializable and save it to Bundle.

If our “detective” does not have to change his state in the process of interacting with the component, then you can not redefine the method onSaveInstanceState(Bundle outState)and Dialog onCreateDialog(Bundle savedInstanceState)restore the dependency from the arguments when calling the method .

Let's look at the implementation:

public class ExampleDialogFragment extends DialogFragment {
    private static final String LISTENER_PROVIDER = "listener_provider";
    private Listener listener;
    public interface ListenerProvider extends Serializable {
        Listener from(DialogFragment dialogFragment);       
    }
    public interface Listener {
        void onMessageEntered(String msg);
    }
    public static DialogFragment newInstance(ListenerProvider provider) {
        ExampleDialogFragment dialogFragment = new ExampleDialogFragment();
        Bundle args = new Bundle();
        args.putSerializable(LISTENER_PROVIDER, provider);
        dialogFragment.setArguments(args);
        return dialogFragment;
    }
    @Override 
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Bundle args = getArguments();
        if(args == null || !args.containsKey(LISTENER_PROVIDER)) {
            throw new IllegalStateException("Listener provider is missing");
        } 
        ListenerProvider listenerProvider =
                (ListenerProvider) args.getSerializable(LISTENER_PROVIDER);
        Listener listener = listenerProvider.from(this);
        ...
    }
}

In this case, the code of our activity will take the form:

public class ExampleActivity extends AppCompatActivity
    implements ExampleDialogFragment.Listener {
    @Override
    public void onMessageEntered(String msg) {
        // TODO
    }
    void showDialog() { 
        DialogFragment dialogFragment = ExampleDialogFragment
            .newInstance(new ListenerProvider()); 
        dialogFragment.show(getFragmentManager(), "dialog");
    }
    private static class ListenerProvider 
        implements ExampleDialogFragment.ListenerProvider { 
        private static final long serialVersionUID = -5986444973089471288L;
        @Override 
        public ExampleDialogFragment.Listener from(DialogFragment dialogFragment) {
            return (ExampleDialogFragment.Listener) dialogFragment.getActivity();
        } 
    }
}

If we need to implement the display of the dialogue from the fragment, we get the following code:

public class ExampleFragment extends Fragment
    implements ExampleDialogFragment.Listener {
    @Override
    public void onMessageEntered(String msg) {
        // TODO
    }
    void showDialog() { 
        DialogFragment dialogFragment = ExampleDialogFragment.newInstance(new ListenerProvider()); 
        dialogFragment.show(getFragmentManager(), "dialog");
    }
    private static class ListenerProvider implements ExampleDialogFragment.ListenerProvider { 
        private static final long serialVersionUID = -5986444973089471288L;
        @Override 
        public ExampleDialogFragment.Listener from(DialogFragment dialogFragment) {
            return (ExampleDialogFragment.Listener) dialogFragment.getParentFragment();
        } 
    }
}

As a result, we get that the calling component itself helps the dialogue find the listener. At the same time, the fragment of the concept has no idea who it is (the listener). For example, if for some reason we don’t want to contact Activitydirectly, then no one bothers us to simply throw the event, and then catch it and process it in the right place (the dialogue code does not even need to be changed):

public class ExampleFragment extends Fragment {
    void onMessageEvent(Message message) {
        // TODO
    }
    void showDialog() { 
        DialogFragment dialogFragment = ExampleDialogFragment.newInstance(new ListenerProvider()); 
        dialogFragment.show(getFragmentManager(), "dialog");
    }
    private static class Message {
        public final String content;
        private Message(String content) {
            this.content = content;  
        }
    }
    private static class ListenerProvider implements ExampleDialogFragment.ListenerProvider { 
        private static final long serialVersionUID = -5986444973089471288L;
        @Override 
        public ExampleDialogFragment.Listener from(DialogFragment dialogFragment) {
            return new ExampleDialogFragment.Listener() {
                @Override
                public void onMessageEntered(String msg) {
                    EventBus.getDefault().post(new Message(msg));
                }
            };
        } 
    }
}

I sort of sorted out the dialogs. We are going further.

Search for dependencies in fragments


Often it is necessary to organize interaction of the type Activity <-> Fragmentor Fragment <-> Fragmentwithin the framework of one activity. The general principle remains the same as described above: through the interface (for example, Listener) and the “detective”, communication between the components is organized. In this article, we will consider one-way interaction.

As an example, we will analyze the case of receiving a presenter in a fragment. I think each of us has come across the following:

public interface Presenter {
    ...
}
public class ExampleFragment extends Fragment {
    private Presenter presenter;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        presenter = App.get().getExampleFragmentComponent().getPresenter();
    }
}

And everything seems to be fine, we hid the creation of dependencies, but their similar receipt is now scattered throughout the project. In my opinion, at least the one who calls should either provide these dependencies or help find them.

Again, we apply our technique with the "detective":

public class ExampleFragment extends Fragment {
    private static final String DI_PROVIDER = "di_provider";
    private Presenter presenter;
    public interface DependencyProvider implements Serializable {
        Presenter getPresenterOf(Fragment fragment);
    }
    public static Fragment newInstance(DependencyProvider dependencyProvider) {
        Fragment fragment = new ExampleFragment();
        Bundle args = new Bundle();
        args.putSerializable(DI_PROVIDER, dependencyProvider);
        fragment.setArguments(args);
        return fragment;
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Bundle args = getArguments();
        if(args == null || !args.containsKey(DI_PROVIDER)) {
            throw new IllegalStateException("DI provider is missing");
        } 
        DependencyProvider diProvider =
                (DependencyProvider) args.getSerializable(DI_PROVIDER);
        presenter = diProvider.getPresenterOf(this);
    }
}
public class ExampleActivity extends AppCompatActivity {
    void showFragment() { 
        FragmentTransaction ft = getFragmentManager().beginTransaction();
        Fragment fragment = ExampleFragment
            .newInstance(new DiProvider()); 
        ft.add(R.id.container, fragment);
        ft.commit();
    }
    private static class DiProvider 
        implements ExampleFragment.DependencyProvider { 
        private static final long serialVersionUID = -5986444973089471288L;
        @Override 
        public Presenter get(Fragment fragment) {
            return App.get().getExampleFragmentComponent().getPresenter();
        } 
    }
}

Thus, we have made our fragment more universal; accordingly, we can easily transfer it from project to project without changing the component code unnecessarily.

In a similar way, you can organize the receipt of dependencies in Activity.

A small example with the implementation of such an approach lies here .

I hope that the described approach will be useful to you in the implementation of projects and will benefit.
Thanks for attention!

Also popular now: