Обнаружение зависимостей Android компонентов

в 14:09, , рубрики: android, Разработка под android

Это не очередная статья про Dagger и его возможности. Не будет ни слова про другие DI фреймворки.

scan

Цель данной публикации — продемонстрировать подход к получению зависимостей во фрагментах, диалогах и активити.

Установка слушателя для диалога

В одном из проектов, перешедших по наследству, наткнулся на следующую реализацию диалога:

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();
        }
    }
}

«А с каких это пор компонент должен заниматься поиском слушателя» — думал я в тот момент.
Давайте сделаем так, чтобы фрагмент не знал, кто конкретно реализует интерфейс слушателя.
Многие могут сразу предложить, например, такой вариант:

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;
    }
}

и код активити, в которую будем встраивать данный диалог:

public class ExampleActivity extends AppCompatActivity {

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

У данного решения есть один существенный недостаток. При изменении конфигурации (к примеру, переворот экрана) получим следующую цепочку: диалог сохранит свое состояние в Bundle и будет уничтожен -> активити будет удалена -> новый экземпляр активити будет создан -> диалог будет создан заново на основе сохраненного в Bundle состояния. В итоге, мы потеряем ссылку на слушателя в диалоге, так как она явно не была сохранена и восстановлена. Мы, конечно, можем вручную вызвать setListener() в одном из колбэков жизненного цикла активити, но есть и другой вариант. Так как анонимный класс мы не можем сохранить в Bundle, равно как и экземпляры обычных классов, нам нужно нужно соблюсти следующие условия:

  1. Слушатель должен реализовать интерфейс Serializable или Parcelable
  2. Передать слушателя через аргументы фрагмета при его создании setArguments(Bundle args)
  3. Слушатель должен быть сохранен в методе onSaveInstanceState(Bundle outState)
  4. Слушатель должен быть восстановлен в методе Dialog onCreateDialog(Bundle savedInstanceState)

Как правило интерфейс слушателя реализуют такие компоненты Android как Activity или Fragment. Подобные комоненты не предназначены для сохранения в Bundle, поэтому нам надо найти другой подход к решению. Давайте попробуем передавать не самого слушателя, а «сыщика»(Provider), который способен его найти. В этом случае, нам никто не помешает сделать его сериализуемым и сохранять в Bundle.

Если наш «сыщик» не должен менять свое состояние в процессе взаимодействия с компонентом, то можно не переопределять метод onSaveInstanceState(Bundle outState) и при вызове метода Dialog onCreateDialog(Bundle savedInstanceState) восстанавливать зависимость из аргументов.

Давайте посмотрим на реализацию:

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);
        ...
    }
}

В таком случае код нашей активити примет вид:

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();
        } 
    }
}

Если нам понадобится реализовать показ диалога из фрагмента, то получим слудеющий код:

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();
        } 
    }
}

В итоге мы получаем, что вызывающий компонент сам помогает диалогу найти слушателя. При этом фрагмент понятия не имеет кто именно им (слушателем) является. К примеру, если по какой-либо причине нам не хочется обращаться к Activity напрямую, то никто нам не мешает просто кинуть событие, а затем в нужном месте поймать его и обработать (код диалога даже не потребуется менять):

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));
                }
            };
        } 
    }
}

С диалогами вроде разобрались. Едем дальше.

Поиск зависимостей во фрагментах

Часто приходится организовывать взаимодействие типа Activity <-> Fragment или Fragment <-> Fragment в рамках одной активити. Общий принцип остается тот же, что был описан выше: посредством интерфейса (к примеру, Listener) и «сыщика» организуется общение между компонентами. В рамках данной статьи будем рассматривать одностороннее взаимодействие.

В качестве примера, разберем случай с получением презентера во фрагменте. Думаю, каждый из нас сталкивался с подобным:

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();
    }
}

И все вроде хорошо, создание зависимостей мы спрятали, а вот подобное их получение теперь разбросано по всему проекту. На мой взгляд, как минимум тот, кто вызывает, должен либо предоставить эти зависимости, либо помочь их найти.

Опять применим наш прием с «сыщиком»:

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();
        } 
    }
}

Таким образом, мы сделали наш фрагмент более универсальным, соответственно, можем без проблем переносить его из проекта в проект не меняя код компонента без надобности.

Аналогичным способом можно организовать получение зависимостей в Activity.

Небольшой пример с реализацией подобного подхода лежит здесь.

Надеюсь, описанный подход пригодится Вам в реализации проектов и принесет пользу.
Спасибо за внимание!

Автор: Константин

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js