Best practice по клиент-серверным приложениям для Android?

Как в 2018 принято делать клиент-серверные приложения на Android? Не могу нагуглить нормальный best practice. Везде какой-то зоопарк уродов. AsyncTask, AsyncTaskLoader, IntentService, кастомные говнообертки типа RoboSprice, Pattern A\B\C и т.п.

Какой на данный момент best practice? Делать IntentService, класть в базу, а потом через BroadcastReceiver уведоблямь Activity и читать обратно из базы? Выглядит не очень адекватно. Может быть есть более вменеяемые способы?

В нормальных ОС все просто: делаешь асинхронный запрос в коллбеке, сохраняешь в некий локальный кеш, затем обновляешь нужный UI. В андроиде же с этим куча проблем, поворот активити и т.п.

Хочется максимально простое решение, с минимумом мусорного кода, костылей, оверинжиниринга и жирных зависимостей ипа RxJava и всякой адовой ерунды типа MVP. Удивительно что чтобы сделать тривиальную задачу, приходится тащить всякие костыли.
  • Вопрос задан
  • 720 просмотров
Пригласить эксперта
Ответы на вопрос 5
@Simipa
React Native developer
RxJava + RxAndroid
Ответ написан
@awesomer
брать за основу что-то уже готовое типа Google Firebase
и см. примеры к нему.
Ответ написан
Комментировать
VYakushev
@VYakushev
Разработчик Android в Nowtaxi
Если для вас MVP и Rxjava - костыли, то посмотрите в сторону гугловых ViewModel, LiveData и т.п. На сайте для андроид-разработчиков есть примеры использования. Правда, у вас выйдет опять "костыль" в виде MVVM от гугла, но зато никаких проблем с поворотом экрана.
Ответ написан
@Dmtm
Android
если "максимально простое решение" это без библиотек, то
это запросы в Thread, Thread хранить в Application, дата слой - синглтон также с привязкой к Application
дата слой - мап вида <url, object>где object - десериализованный ответ
бродкасты для отсылки сообщений о результатах запроса (если не хочется возиться с бродкастами, то есть EventBus)

т.е. запрос меняет данные, дата слой оповещает об изменении, потребитель сам забирает данные

для простых проектов сойдет, но потом начнутся сложности
типа атомарность бизнес операции состоящей из нескольких запросов (привет RxJava) или сохранение состояния экрана (в т.ч. с динамическими view) - (привет Moxy), даже просто дождаться появления интернета и заново выполнить запрос из очереди - все самому писать придется
Ответ написан
@ilitaexperta Автор вопроса
Вообщем я нашел приемлимый подход, может кому пригодится:

  1. Делаем слой для запросов. Все поля статические. В нем делаем список листенеров и метод для создания запроса через AsynTask. В ответе асинк такски обходим список листенеров и вызываем нужный коллбек.
  2. Делаем слой для данных(тоже все статиком) и подписываем его на ответы(добавляем листенер) с наивысшим приоритетом(т.е. чтобы первым вызывался листенер в слое данных)
  3. Нужные активити подписываем на ответы в onResume и отписываемся в onPause.
  4. Данные всегда берем только из слоя данных, а активити обновляем по коллбекам из листенера и в onResume


Таким образом:
  1. Не будет утечек т.к. AsyncTask содержит ссылки только на статики
  2. Ответ на запрос не придет в мертвую\запауженую активити,
  3. Ответ не потеряется и данные сохраняется в слое данных.
  4. Если активити была на паузе или умерла, она подсосет данные из дата слоя в onResume
  5. Если активити пересоздалась, то ответ придет уже в новую. А если ответ придет между убийством старой и созданием новой, то см. пункты 3 и 4
  6. Никаких сторонних костылей и оверинжиниринга, можно даже чуток усложнить в некоторых местах(Наример сделать отдельный листенер на изменение данных в дата слое и подписывать активити на него т.е. как писали выше "запрос меняет данные, дата слой оповещает об изменении, потребитель сам забирает данные")


Накидал упрощенный пример кода, некоторые места опущены для простоты.

Слой для запросов
public class Requests
{
	public interface OnCompleteListener
	{
		default void onSomeMethod1(JsonElement answer) {}
		default void onSomeMethod2(JsonElement answer) {}

		default int getPriority() { return 0; }
	}

	private static class RequestTask extends AsyncTask<Void, Void, String>
	{
		public RequestTask(String method)
		{
			this.method = method;
		}

		private String method;

		@Override
		protected String doInBackground(Void ... params)
		{
			// Тут делаем запрос к серверу
			// ...
		}

		@Override
		protected void onPostExecute(String result)
		{
			handleAnswer(result);
		}
	}

	private static List<OnCompleteListener> listeners = new ArrayList<>();

	private static void handleAnswer(String answer)
	{
		for(OnCompleteListener listener : listeners)
		{
			if(methodName.equals("api/method1")) listener.onSomeMethod1(answer);
			else if(methodName.equals("api/method2")) listener.onSomeMethod2(answer);
                }
	}

	private static void makeRequest(String method)
	{
		new RequestTask(method).execute();
	}

	public static void registerListener(OnCompleteListener listener)
	{
		listeners.add(listener);

		Collections.sort(listeners, (l1, l2) -> l2.getPriority() - l1.getPriority());
	}

	public static void unregisterListener(OnCompleteListener listener)
	{
		listeners.remove(listener);
	}

	public static void method1()
	{
		makeRequest("api/method1");
	}

	public static void method2()
	{
		makeRequest("api/method2");
	}
}


Активити
public class MainActivity extends AppCompatActivity implements Requests.OnCompleteListener
{
	// ...

	@Override
	protected void onPause()
	{
		super.onPause();

		Requests.unregisterListener(this);
	}

	@Override
	protected void onResume()
	{
		super.onResume();

		Requests.registerListener(this);
		updateView();
	}

	private void updateView()
	{
		String someData = Data.getSomeData();

		// Тут обновляем вьюшки в активити
		// ...
	}

	@Override
	public void onMethod1(String answer)
	{
		updateView();
	}
}


Слой данных
public class MyApp extends Application
{
	@Override
	public void onCreate()
	{
		super.onCreate();
		
		Requests.registerListener(Data.listener);
	}
}

public class Data
{
	// Просто какие-то данные для примера
	private static List<String> someDataStorage = new ArrayList<>();

	public static String getSomeData()
	{
		return someDataStorage.last();
	}

	public static Requests.OnCompleteListener listener = new Api.OnCompleteListener()
	{
		@Override
		public void onMethod1(String answer)
		{
			someDataStorage.add(answer);
		}

		public void onMethod2(String answer)
		{
			someDataStorage.add(answer);
		}

		@Override
		public int getPriority()
		{
			return Integer.MAX_VALUE;
		}
	};
}
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы