Android MVP: безопасное использование контекста в Presenter

В моем приложении я работаю с ContentProvider и использую LoaderManager.LoaderCallbacks<Cursor>.

Фрагмент (представление)

public class ArticleCatalogFragment extends BaseFragment
        implements ArticleCatalogPresenter.View,
        LoaderManager.LoaderCallbacks<Cursor> {

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        return onCreateArticleCatalogLoader(args);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {        
         data.registerContentObserver(new LoaderContentObserver(new Handler(), loader));
         updateUI(data);        
    }   

    private Loader onCreateArticleCatalogLoader(Bundle args) {
            int categoryId = args.getInt(CATEGORY_ID);
            Loader loader = new ArticleCatalogLoader(this.getActivity(), categoryId);            
            return loader;
    }

}

С точки зрения MVP мне нужно:

Ведущий

public class ArticleCatalogPresenter extends BasePresenter
        implements LoaderManager.LoaderCallbacks<Cursor> {

    View view;

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        return onCreateArticleCatalogLoader(args);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {        
         data.registerContentObserver(new LoaderContentObserver(new Handler(), loader));
         view.updateUI(data);        
    }               

    private Loader onCreateArticleCatalogLoader(Bundle args) {    
            int categoryId = args.getInt(CATEGORY_ID);
            Loader loader = new ArticleCatalogLoader(context, categoryId); // need Context
            return loader;
    }


    interface View {
        updateUI(Cursor data)
    }

}

Итак, мне нужен контекст в Presenter.

Есть некоторые нюансы:

  1. Ведущий знает о Контексте - это плохо, Ведущий не должен знать об Андроиде.

  2. Наличие контекста в Presenter может привести к утечке памяти.

Теперь я беспокоюсь о том, как избежать таких проблем, как утечка памяти, и как лучше передать контекст в Presenter, использовать контекст приложения или действие/фрагмент?


person Alexandr    schedule 11.08.2015    source источник
comment
Контекст приложения — это путь. Если представлению нужен контекст действия, оно может сохранить его самостоятельно (переданное в конструкторе), просто убедитесь, что у вас нет строгой ссылки на представление (независимо от того), если ваш презентатор пережил действие/фрагмент.   -  person JohanShogun    schedule 11.08.2015
comment
Другая мысль заключается в том, что вы можете позволить своей активности/фрагменту взять на себя роль ведущего. Мне кажется, что вы заставили свой фрагмент взять на себя роль представления, это немного странно, поскольку базовая функциональность фрагмента довольно сильно превышает функциональность ведущего. Ваше представление находится в файлах xml и представлении подклассов.   -  person JohanShogun    schedule 12.08.2015
comment
Спасибо за отзыв. У меня есть ситуация, когда часть бизнес-логики в Presenter (самая большая часть), а другая часть во Fragment (работа с CursorLoader), и это создает проблемы. Я хочу перенести всю бизнес-логику в Presenter.   -  person Alexandr    schedule 12.08.2015
comment
Как указано выше, сделайте свой фрагмент докладчиком, а не представлением. Шаблон mvp подходит только для Android из-за того, как настроены действия/фрагменты. Ваше представление - файлы xml (и т.д.). Кроме того, рассмотрите возможность создания механизма бизнес-правил для вашей бизнес-логики вместо того, чтобы разбрасывать его по классу, ответственному за представление его в представлении. Для записи адаптер курсора - это не бизнес-логика, это логика презентатора.   -  person JohanShogun    schedule 12.08.2015
comment
Привет @Alexandr, ты нашел какое-нибудь хорошее решение?   -  person Sergey B.    schedule 28.02.2016
comment
Привет! Сейчас я изменил архитектуру, и у меня нет контекста в презентере. Но если бы такая проблема была, я бы хотел использовать внедрение зависимостей и крестик. Dagger поможет вам внедрить контекст в любое место, и легко убедиться, что это контекст приложения.   -  person Alexandr    schedule 28.02.2016
comment
Эй, если вы используете конкретный тип представления вместо интерфейса, вы можете просто вызывать mView.getContext() всякий раз, когда это необходимо.   -  person WindRider    schedule 01.04.2016
comment
Я заставил ведущего вызвать метод из моего интерфейса просмотра, который возвращает CursorLoader, но я не знаю, хорошее ли это решение...   -  person Cassio Landim    schedule 21.09.2016
comment
Это должно помочь stackoverflow.com/questions/39100105 /   -  person fvolodimir    schedule 21.06.2017


Ответы (4)


Добавлять контекст в Presenter нехорошо, так как Presenter отвечает за бизнес-логику. Чтобы иметь дело с контекстом, вам нужно, чтобы фрагмент/действия использовали обратные вызовы с помощью интерфейсов, которые будут указывать, какие действия должны выполняться действием/фрагментом при работе с представлениями. Фрагмент/действия отвечают за предоставление контекста.

Пример:

interface BaseContract {
        interface BaseView {
            //Methods for View
            void onDoSomething();
        }

        interface BasePresenter {
            void doSomething();

        }
    }

    class BaseMainPresenter implements BaseContract.BasePresenter {
        BaseContract.BaseView view;

        BaseMainPresenter(BaseContract.BaseView view) {
            this.view = view;
        }

        @Override
        public void doSomething() {
            if (view != null)
                view.onDoSomething();
        }
    }

    class DemoClass implements BaseContract.BaseView {

        //Create object of Presenter 

        /****
         * Example :
         * BaseMainPresenter baseMainPresenter = new BaseMainPresenter(this);
         */
        @Override
        public void onDoSomething() {
            //Deal with Context here.
        }
    }
person Archis Thakkar    schedule 18.02.2017
comment
Не могли бы вы уточнить, как это работает с обратными вызовами Loaders в соответствии с вопросом OP? Спасибо. - person trocchietto; 12.04.2017

Просто не регистрируйте докладчика в качестве цели обратного вызова для Android (например, BroadcastReceiver, LoaderManager.LoaderCallbacks и т. д.). Обрабатывайте методы обратного вызова в своем представлении (фрагмент или действие) и передайте все связанные данные докладчику.

Если вам нужно Context для создания объекта, пусть ваше представление создаст этот объект (поскольку оно имеет ссылку на Context). В вашем случае вызов

Loader loader = new ArticleCatalogLoader(context, categoryId)

должен быть рефакторинг для

view.createLoaderForCategory(categoryId)
person artkoenig    schedule 13.06.2017

Такой код

Loader loader = new ArticleCatalogLoader(context, categoryId);

приводит к непроверяемому коду. Вы должны избегать создания «бизнес-объектов» в своем коде и позволять кому-то другому делать это за вас (любая структура DI, такая как Кинжал 2 был бы лучшим вариантом, чем обращаться с ним самостоятельно)

Сказав это, ваша проблема - это то, что DI решил давным-давно. Вам нужен свежий новый экземпляр любого объекта? Используйте Provider

Provider — это объект, который «предоставляет» экземпляры объектов. Поэтому вместо того, чтобы иметь

Loader loader = new ArticleCatalogLoader(context, categoryId);

у тебя будет

Loader loader = loaderProvider.get(categoryId);

Итак, единственное, что вам нужно, это что-то вроде этого:

public class ArticleCatalogPresenter ... {
    ...
    private final Provider<Loader> loaderProvider;

    public ArticleCatalogPresenter(Provider<Loader> loaderProvider, ...) {
        this.loaderProvider = loaderProvider;
        ...
    }

    private Loader onCreateArticleCatalogLoader(Bundle args) {    
        int categoryId = args.getInt(CATEGORY_ID);
        Loader loader = loaderProvider.get(categoryId); // no context needed anymore!
        return loader;
    }

}
person Pelocho    schedule 13.06.2017

public class ArticleCatalogPresenter extends BasePresenter
        implements LoaderManager.LoaderCallbacks<Cursor> {

    View view;             
    ...
    private Loader onCreateArticleCatalogLoader(Bundle args) {    
            int categoryId = args.getInt(CATEGORY_ID);
            Loader loader = new ArticleCatalogLoader(context, categoryId); // need Context
            return loader;
    }
}

Итак, вы хотите, чтобы context в вашем Presenter создал новый экземпляр ArticleCatalogLoader. Верно?

Если это так, передайте экземпляр Presenter через конструктор. Итак, в контейнере Activity или DI, когда вы хотите создать объект Presenter, сделайте что-то вроде:

ArticleCatalogPresenter articleCatalogPresenter=new ArticleCatalogPresenter(articleCatalogView,new ArticleCatalogLoader(context,categoryId));

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

Что касается вашего беспокойства по поводу утечки памяти, вы можете легко избежать этого, прослушав onStop() в своем View, а затем вызвав соответствующий метод в вашем Presenter, чтобы отменить любой сетевой запрос или context зависимый задача.

Я написал библиотеку MVP, которая очень помогает сэкономить количество шаблонов, необходимых для MVP, а также предотвращение утечек памяти.

person Ali Nem    schedule 20.04.2018