Русский
Русский
English
Статистика
Реклама

Пустой экран

Экраны отсутствующего контента в мобильном приложении на примере Xamarin

08.01.2021 14:08:13 | Автор: admin

С чего все началось

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

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

Первые шаги к улучшению ситуации

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

Сделали для этого вью-модель, и соответствующие вьюхи под каждую платформу. Ниже приведу код вью-модели, платформенный UI-код не буду постить из-за громоздкости.

public class EmptyStateViewModel : ViewModel{    public EmptyStateViewModel(string image, string title, string description)    {        Image = image;        Title = title;        Description = description;    }    public string Image { get; }    public string Title { get; }    public string Description { get; }}

Следующим шагом на платформах(или в xaml в случае для Xamarin Forms) нужно прописать Bindings на проперти вью модели, в зависимости от того, какой mvvm-фреймворк используется.

А что дальше?

А далее возник вопрос - что делать, если по каким-то причинам наш запрос на бекенд фейлится. И тут пришла мысль - переиспользовать уже готовый EmptyStateView, но добавить внизу кнопку Retry, для возможности повторной отправки запроса. Потому мы просто наследовались от EmptyStateViewModel, но добавили поля с текстом кнопки, и командой на клик.

public class ErrorStateViewModel : EmptyStateViewModel{    public ErrorStateViewModel(string image, string title, string description, string actionTitle, Command actionCommand)        : base(image, title, description)    {        ActionTitle = actionTitle;        ActionCommand = actionCommand;    }    public string ActionTitle { get; }    public Command ActionCommand { get; }}

И как все это использовать?

После этого пришло время задуматься - как это менеджить. И тут неплохим показался вот такой вариант. Создаем фабрику, которая будет нам возвращать нужную вью-модель. А так же метод None, который будет возвращать null.

public static class OverlayFactory{    public static T None<T>()        where T : EmptyStateViewModel    {        return null;    }    public static EmptyStateViewModel CreateCustom(string image, string title, string description)    {        return new EmptyStateViewModel(image, title, description);    }    public static ErrorStateViewModel CreateCustom(string image, string title, string description, string actionTitle, Command actionCommand)    {        return new ErrorStateViewModel(image, title, description, actionTitle, actionCommand);    }}

Как результат - мы можем во вью-модели, где мы грузим список, легко обрабатывать отображение пустой вьюхи, либо же вьюхи ошибки

public class SomeViewModel : BaseViewModel{    private IItemsLoadingService _itemsLoadingService;        public SomeViewModel(IItemsLoadingService itemsLoadingService)    {        _itemsLoadingService = itemsLoadingService;    }    public ObservableCollection<ItemViewModel> Items { get; } = new ObservableCollection<ItemViewModel>();    public EmptyStateViewModel EmptyState { get; protected set; }    public ErrorStateViewModel ErrorState { get; protected set; }    public override async Task InitializeAsync()    {        await base.InitializeAsync();        await LoadItemsAsync();    }    private async Task LoadItemsAsync()    {        try        {            var result = await _itemsLoadingService.GetItemsAsync();            var items = result.ToList();            ErrorState = OverlayFactory.None<ErrorStateViewModel>();            if (items.Count == 0)            {                EmptyState = OverlayFactory.CreateCustom("img_empty_state", "Title", "Description");            }            else            {                EmptyState = OverlayFactory.None<ErrorStateViewModel>();                // Add items to list            }        }        catch        {            ErrorState = OverlayFactory.CreateCustom("img_error_state", "Title", "Description", "Retry", new Command(() => LoadItemsAsync));        }    }}

На платформах же нам необходимо прописать кастомный Binding для EmptyState/ErrorState вьюх на соответствующие вью модели, в зависимости от используемого mvvm-фреймворка, и проверять, если у нас EmptyStateViewModel/ErrorStateViewModel null, то скрывать соответствующую вьюху. Для этого в нашем случае использовался простой метод SetViewModel.

Для андроида тут все просто, при задании для View ее ViewModel мы установим View уже существующий ViewState из коробки. Если ViewModel null - тогда попросту задаем ViewState Gone, если существует - то Visible:

public void SetViewModel(EmptyStateViewModel viewModel){    ViewModel = viewModel;    View.Visibility = viewModel != null ? ViewStates.Visible : ViewStates.Gone;}

Для iOS немного сложнее - необходимо деактивировать constraints для вьюхи, а только потом - прятать ее. Для начала добавим enum, аналогичный стандартному из Android.

public void SetViewModel(EmptyStateViewModel viewModel){    ViewModel = viewModel;    View.SetVisibility(viewModel != null ? ViewStates.Visible : ViewStates.Gone);}

Нам потребуется несколько extension методов

public static void SetVisibility(this UIView view, ViewVisibility visibility){    var constraints = GetViewConstraints(view) ?? new NSLayoutConstraint[] {};    if (visibility == ViewVisibility.Gone)    {        SaveViewConstraints(view, constraints);        NSLayoutConstraint.DeactivateConstraints(constraints);        view.Hidden = true;        return;    }      if (visibility == ViewVisibility.Visible)    {        SaveViewConstraints(view, null);        NSLayoutConstraint.ActivateConstraints(constraints);        view.Hidden = false;        return;    }}

Тут мы в случае установки ViewVisibility.Gone предварительно сохраняем constraints нашей view и деактивируем их, а при включении видимости - наоборот достаем предварительно сохраненные constraints, обнуляем сохранение, а затем активируем их.

private static NSLayoutConstraint[] GetViewConstraints(UIView view){    return view.GetAssociatedObject<NSMutableArray<NSLayoutConstraint>>(Key)?.ToArray() ??           view.Superview?.Constraints               .Where(constraint => (constraint.FirstItem?.Equals(view) == true) || constraint.SecondItem.Equals(view))               .ToArray();}private static void SaveViewConstraints(UIView view, NSLayoutConstraint[] constraints){    NSMutableArray<NSLayoutConstraint> viewConstraints = null;    if (constraints.Length > 0)    {        viewConstraints = new NSMutableArray<NSLayoutConstraint>();        viewConstraints.AddObjects(constraints);    }    view.SetAssociatedObject(Key, viewConstraints, AssociationPolicy.RetainNonAtomic);}

Первый метод позволяет достать предварительно сохраненные constraints, если такие есть, либо же, если нет, получить текущие. Если же родительский view отсутствует, то вернется null.

Второй метод сохранит текущие constraints, чтоб затем их можно было восстановить.

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

PS - первая статья на Хабре, потому не судите строго. Но нужно же с чего-то начать.

Подробнее..

Удобное отображение пустого списка

31.01.2021 18:12:32 | Автор: admin

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

Суть заключается в том, что обязанность показа вспомогательной информации возлагается на отдельный объект. Это дает более правильное разделение логики и уменьшение кода в активности. Приступим к более подробному описанию реализации.

Каждая подсказа представляет из себя отдельный фрагмент, который может содержать все угодно. Чтобы управлять ими, объекту нужен FragmentManager и Id контейнера, в котором должны находится фрагменты. Больше зависимостей нет.

Каждая ситуация представляет из себя элемент перечисления (все станет очень понятно с кодом), вложенного в этот объект. Когда нужно изменить подсказку, будет вызываться метод, отвечающий за замену, в который будет передаваться новая ситуация. Это вся логика.

Теперь это надо сделать в реалиях Андроида. Кроме самой активности (или фрагмента), которой нужен объект, на него никто больше ссылаться не будет. Есть начальное, Дефолтное, состояние. Оно будет отображаться при первом появлении, пока не появится новое значение. Сами значения хранятся в LiveData (которая во ViewModel), на которую подписывается активность и передает каждое новое объекту. Это позволяет переживать пересоздание активности и сохранять состояние.

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

Нюансы

Если, например, не очистить список от данных, то подсказка может быть отображена поверх (или под) этим списком, что будет некрасиво. Для этого нужно предварительно очищать список (или скрывать его видимость, что лучше?).

Реализация

CodeSwitcher.

CodeSwitcher. Я не смог придумать адекватного названия.

//код самого объектаpublic class CodeSwitcher {    //перечисление всех ситуаций    public enum Code {        DEFAULT,        HTTP_OK,        HTTP_CREATED,        HTTP_BAD_REQUEST,        HTTP_NOT_FOUND,        NO_DATA    }    //зависимости    private FragmentManager fragmentManager;    private int fragmentHostId;    public CodeSwitcher(FragmentManager fragmentManager, int fragmentHostId) {        this.fragmentManager = fragmentManager;        this.fragmentHostId = fragmentHostId;    }    //метод, который будет заменять фрагменты    public void switchFragments(Code code) {        FragmentTransaction transaction = fragmentManager.beginTransaction();        switch (code) {            case HTTP_OK:                transaction.replace(fragmentHostId, CodeFragment.newInstance("HTTP_OK"));                break;            case HTTP_CREATED:                transaction.replace(fragmentHostId, CodeFragment.newInstance("HTTP_CREATED"));                break;            case HTTP_BAD_REQUEST:                transaction.replace(fragmentHostId, CodeFragment.newInstance("HTTP_BAD_REQUEST"));                break;            case HTTP_NOT_FOUND:                transaction.replace(fragmentHostId, CodeFragment.newInstance("HTTP_NOT_FOUND"));                break;            case NO_DATA:                transaction.replace(fragmentHostId, CodeFragment.newInstance("NO_DATA"));                break;            default:                transaction.replace(fragmentHostId, CodeFragment.newInstance("Default"));                break;        }        transaction.commit();    }}
//код ViewModel//в конструкторе происходит установка дефолтного состоянияpublic CodeShowActivityViewModel() {    listCode = new MutableLiveData<>();    listCode.setValue(CodeSwitcher.Code.DEFAULT);}//методы только для примера, в реальности таких нет, нужны для изменения состоянияpublic void httpOk() {  listCode.setValue(CodeSwitcher.Code.HTTP_OK);  clearList();}public void httpBadRequest() {  listCode.setValue(CodeSwitcher.Code.HTTP_BAD_REQUEST);  clearList();}
//код активности, необходимый для использованияprivate CodeSwitcher switcher;//в onCreate()switcher = new CodeSwitcher(getSupportFragmentManager(), R.id.айди_контейнера);//подписка на LiveData, передача значения на обработкуcodeActVM.getListCode().observe(this, code -> {  switcher.switchFragments(code);});
Гифка (4мб) с описанием

С самого начала появляется дефолтное состояние, оно специально отображается на фоне, но в реальности должно быть пустым. Затем эмулируется получение различных кодов и их отображение. В конце показано, что состояние сохраняется и при пересоздании активности.

Что думаете об этом способе?

P.S.

Беды с названиями...

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru