From 0dede1ba7d00d8a9f60afc446f8cdd9ee52f4ff9 Mon Sep 17 00:00:00 2001 From: charlag Date: Sun, 5 Nov 2017 19:11:00 +0300 Subject: [PATCH] Make more clear representation of placeholder in notifications --- .../tusky/adapter/NotificationsAdapter.java | 65 +++---- .../tusky/entity/Notification.java | 1 - .../tusky/fragment/NotificationsFragment.java | 164 +++++++++++------- .../tusky/util/CollectionUtil.java | 38 ++++ .../com/keylesspalace/tusky/util/Either.java | 125 +++++++++++++ .../tusky/util/ViewDataUtils.java | 6 +- .../tusky/viewdata/NotificationViewData.java | 76 ++++---- 7 files changed, 348 insertions(+), 127 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/CollectionUtil.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/Either.java diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 2fad7cb7f..dfbb8e211 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -101,11 +101,19 @@ public class NotificationsAdapter extends RecyclerView.Adapter { public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { if (position < notifications.size()) { NotificationViewData notification = notifications.get(position); - Notification.Type type = notification.getType(); + if (notification instanceof NotificationViewData.Placeholder) { + NotificationViewData.Placeholder placeholder = ((NotificationViewData.Placeholder) notification); + PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder; + holder.setup(!placeholder.isLoading(), statusListener); + return; + } + NotificationViewData.Concrete concreteNotificaton = + (NotificationViewData.Concrete) notification; + Notification.Type type = concreteNotificaton.getType(); switch (type) { case MENTION: { StatusViewHolder holder = (StatusViewHolder) viewHolder; - StatusViewData status = notification.getStatusViewData(); + StatusViewData status = concreteNotificaton.getStatusViewData(); holder.setupWithStatus(status, statusListener, mediaPreviewEnabled); break; @@ -113,23 +121,18 @@ public class NotificationsAdapter extends RecyclerView.Adapter { case FAVOURITE: case REBLOG: { StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder; - holder.setMessage(type, notification.getAccount().getDisplayName(), - notification.getStatusViewData()); - holder.setupButtons(notificationActionListener, notification.getAccount().id); - holder.setAvatars(notification.getStatusViewData().getAvatar(), - notification.getAccount().avatar); + holder.setMessage(type, concreteNotificaton.getAccount().getDisplayName(), + concreteNotificaton.getStatusViewData()); + holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().id); + holder.setAvatars(concreteNotificaton.getStatusViewData().getAvatar(), + concreteNotificaton.getAccount().avatar); break; } case FOLLOW: { FollowViewHolder holder = (FollowViewHolder) viewHolder; - holder.setMessage(notification.getAccount().getDisplayName(), - notification.getAccount().username, notification.getAccount().avatar); - holder.setupButtons(notificationActionListener, notification.getAccount().id); - break; - } - case PLACEHOLDER: { - PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder; - holder.setup(!notification.isPlaceholderLoading(), statusListener); + holder.setMessage(concreteNotificaton.getAccount().getDisplayName(), + concreteNotificaton.getAccount().username, concreteNotificaton.getAccount().avatar); + holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().id); break; } } @@ -150,21 +153,25 @@ public class NotificationsAdapter extends RecyclerView.Adapter { return VIEW_TYPE_FOOTER; } else { NotificationViewData notification = notifications.get(position); - switch (notification.getType()) { - default: - case MENTION: { - return VIEW_TYPE_MENTION; - } - case FAVOURITE: - case REBLOG: { - return VIEW_TYPE_STATUS_NOTIFICATION; - } - case FOLLOW: { - return VIEW_TYPE_FOLLOW; - } - case PLACEHOLDER: { - return VIEW_TYPE_PLACEHOLDER; + if (notification instanceof NotificationViewData.Concrete) { + NotificationViewData.Concrete concrete = ((NotificationViewData.Concrete) notification); + switch (concrete.getType()) { + default: + case MENTION: { + return VIEW_TYPE_MENTION; + } + case FAVOURITE: + case REBLOG: { + return VIEW_TYPE_STATUS_NOTIFICATION; + } + case FOLLOW: { + return VIEW_TYPE_FOLLOW; + } } + } else if (notification instanceof NotificationViewData.Placeholder) { + return VIEW_TYPE_PLACEHOLDER; + } else { + throw new AssertionError("Unknown notification type"); } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.java b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.java index 6aff1520d..267c483ae 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.java +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.java @@ -27,7 +27,6 @@ public class Notification { FAVOURITE, @SerializedName("follow") FOLLOW, - PLACEHOLDER } public Type type; diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index e3f11201e..0755f4023 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -44,6 +44,8 @@ import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.ActionButtonActivity; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.receiver.TimelineReceiver; +import com.keylesspalace.tusky.util.CollectionUtil; +import com.keylesspalace.tusky.util.Either; import com.keylesspalace.tusky.util.HttpHeaderLink; import com.keylesspalace.tusky.util.ListUtils; import com.keylesspalace.tusky.util.PairedList; @@ -74,6 +76,21 @@ public class NotificationsFragment extends SFragment implements MIDDLE } + /** + * Placeholder for the notifications. Consider moving to the separate class to hide constructor + * and reuse in different places as needed. + */ + private static final class Placeholder { + private static final Placeholder INSTANCE = new Placeholder(); + + public static Placeholder getInstance() { + return INSTANCE; + } + + private Placeholder() { + } + } + private SwipeRefreshLayout swipeRefreshLayout; private LinearLayoutManager layoutManager; private RecyclerView recyclerView; @@ -89,11 +106,17 @@ public class NotificationsFragment extends SFragment implements private String bottomId; private String topId; - private final PairedList notifications - = new PairedList<>(new Function() { + // Each element is either a Notification for loading data or a Placeholder + private final PairedList, NotificationViewData> notifications + = new PairedList<>(new Function, NotificationViewData>() { @Override - public NotificationViewData apply(Notification input) { - return ViewDataUtils.notificationToViewData(input); + public NotificationViewData apply(Either input) { + if (input.isRight()) { + Notification notification = input.getAsRight(); + return ViewDataUtils.notificationToViewData(notification); + } else { + return new NotificationViewData.Placeholder(false); + } } }); @@ -185,7 +208,7 @@ public class NotificationsFragment extends SFragment implements ActionButtonActivity activity = (ActionButtonActivity) getActivity(); FloatingActionButton composeButton = activity.getActionButton(); - if(composeButton != null) { + if (composeButton != null) { if (hideFab) { if (dy > 0 && composeButton.isShown()) { composeButton.hide(); // hides the button if we're scrolling down @@ -225,13 +248,12 @@ public class NotificationsFragment extends SFragment implements @Override public void onReply(int position) { - Notification notification = notifications.get(position); - super.reply(notification.status); + super.reply(notifications.get(position).getAsRight().status); } @Override public void onReblog(final boolean reblog, final int position) { - final Notification notification = notifications.get(position); + final Notification notification = notifications.get(position).getAsRight(); final Status status = notification.status; reblogWithCallback(status, reblog, new Callback() { @Override @@ -242,7 +264,9 @@ public class NotificationsFragment extends SFragment implements if (status.reblog != null) { status.reblog.reblogged = reblog; } - notifications.set(position, notification); + // Java's type inference *eyeroll* + notifications.set(position, + Either.right(notification)); adapter.updateItemWithNotify(position, notifications.getPairedItem(position), true); @@ -260,7 +284,7 @@ public class NotificationsFragment extends SFragment implements @Override public void onFavourite(final boolean favourite, final int position) { - final Notification notification = notifications.get(position); + final Notification notification = notifications.get(position).getAsRight(); final Status status = notification.status; favouriteWithCallback(status, favourite, new Callback() { @Override @@ -272,7 +296,8 @@ public class NotificationsFragment extends SFragment implements status.reblog.favourited = favourite; } - notifications.set(position, notification); + notifications.set(position, + Either.right(notification)); adapter.updateItemWithNotify(position, notifications.getPairedItem(position), true); @@ -289,7 +314,7 @@ public class NotificationsFragment extends SFragment implements @Override public void onMore(View view, int position) { - Notification notification = notifications.get(position); + Notification notification = notifications.get(position).getAsRight(); super.more(notification.status, view, position); } @@ -301,38 +326,40 @@ public class NotificationsFragment extends SFragment implements @Override public void onViewThread(int position) { - Notification notification = notifications.get(position); + Notification notification = notifications.get(position).getAsRight(); super.viewThread(notification.status); } @Override public void onOpenReblog(int position) { - Notification notification = notifications.get(position); - if (notification != null) onViewAccount(notification.account.id); + Notification notification = notifications.get(position).getAsRight(); + onViewAccount(notification.account.id); } @Override public void onExpandedChange(boolean expanded, int position) { - NotificationViewData old = notifications.getPairedItem(position); + NotificationViewData.Concrete old = + (NotificationViewData.Concrete) notifications.getPairedItem(position); StatusViewData statusViewData = new StatusViewData.Builder(old.getStatusViewData()) .setIsExpanded(expanded) .createStatusViewData(); - NotificationViewData notificationViewData = new NotificationViewData(old.getType(), - old.getId(), old.getAccount(), statusViewData, false); + NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(), + old.getId(), old.getAccount(), statusViewData); notifications.setPairedItem(position, notificationViewData); adapter.updateItemWithNotify(position, notificationViewData, false); } @Override public void onContentHiddenChange(boolean isShowing, int position) { - NotificationViewData old = notifications.getPairedItem(position); + NotificationViewData.Concrete old = + (NotificationViewData.Concrete) notifications.getPairedItem(position); StatusViewData statusViewData = new StatusViewData.Builder(old.getStatusViewData()) .setIsShowingSensitiveContent(isShowing) .createStatusViewData(); - NotificationViewData notificationViewData = new NotificationViewData(old.getType(), - old.getId(), old.getAccount(), statusViewData, false); + NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(), + old.getId(), old.getAccount(), statusViewData); notifications.setPairedItem(position, notificationViewData); adapter.updateItemWithNotify(position, notificationViewData, false); } @@ -341,13 +368,12 @@ public class NotificationsFragment extends SFragment implements public void onLoadMore(int position) { //check bounds before accessing list, if (notifications.size() >= position && position > 0) { - String fromId = notifications.get(position - 1).id; - String toId = notifications.get(position + 1).id; + // is it safe? + String fromId = notifications.get(position - 1).getAsRight().id; + String toId = notifications.get(position + 1).getAsRight().id; sendFetchNotificationsRequest(fromId, toId, FetchEnd.MIDDLE, position); - - NotificationViewData old = notifications.getPairedItem(position); - NotificationViewData notificationViewData = new NotificationViewData(old.getType(), - old.getId(), old.getAccount(), old.getStatusViewData(), true); + NotificationViewData notificationViewData = + new NotificationViewData.Placeholder(true); notifications.setPairedItem(position, notificationViewData); adapter.updateItemWithNotify(position, notificationViewData, false); } else { @@ -390,10 +416,11 @@ public class NotificationsFragment extends SFragment implements @Override public void removeAllByAccountId(String accountId) { // using iterator to safely remove items while iterating - Iterator iterator = notifications.iterator(); + Iterator> iterator = notifications.iterator(); while (iterator.hasNext()) { - Notification notification = iterator.next(); - if (notification.account.id.equals(accountId)) { + Either notification = iterator.next(); + Notification maybeNotification = notification.getAsRightOrNull(); + if (maybeNotification != null && maybeNotification.account.id.equals(accountId)) { iterator.remove(); } } @@ -470,7 +497,7 @@ public class NotificationsFragment extends SFragment implements break; } case MIDDLE: { - insert(notifications, pos); + replacePlaceholderWithNotifications(notifications, pos); break; } case BOTTOM: { @@ -520,24 +547,25 @@ public class NotificationsFragment extends SFragment implements if (uptoId != null) { topId = uptoId; } + List> liftedNew = + liftNotificationList(newNotifications); if (notifications.isEmpty()) { - notifications.addAll(newNotifications); + notifications.addAll(liftedNew); } else { - int index = notifications.indexOf(newNotifications.get(newNotifications.size() - 1)); + int index = notifications.indexOf(liftedNew.get(newNotifications.size() - 1)); for (int i = 0; i < index; i++) { notifications.remove(0); } - int newIndex = newNotifications.indexOf(notifications.get(0)); + + + int newIndex = liftedNew.indexOf(notifications.get(0)); if (newIndex == -1) { - if(index == -1 && newNotifications.size() >= LOAD_AT_ONCE) { - Notification placeholder = new Notification(); - placeholder.type = Notification.Type.PLACEHOLDER; - newNotifications.add(placeholder); + if (index == -1 && liftedNew.size() >= LOAD_AT_ONCE) { + liftedNew.add(Either.left(Placeholder.getInstance())); } - notifications.addAll(0, newNotifications); + notifications.addAll(0, liftedNew); } else { - List sublist = newNotifications.subList(0, newIndex); - notifications.addAll(0, sublist); + notifications.addAll(0, liftedNew.subList(0, newIndex)); } } adapter.update(notifications.getPairedCopy()); @@ -551,9 +579,10 @@ public class NotificationsFragment extends SFragment implements bottomId = fromId; } int end = notifications.size(); - Notification last = notifications.get(end - 1); - if (last != null && !findNotification(newNotifications, last.id)) { - notifications.addAll(newNotifications); + List> liftedNew = liftNotificationList(newNotifications); + Either last = notifications.get(end - 1); + if (last != null && liftedNew.indexOf(last) == -1) { + notifications.addAll(liftedNew); List newViewDatas = notifications.getPairedCopy() .subList(notifications.size() - newNotifications.size(), notifications.size()); @@ -561,23 +590,14 @@ public class NotificationsFragment extends SFragment implements } } - private static boolean findNotification(List notifications, String id) { - for (Notification notification : notifications) { - if (notification.id.equals(id)) { - return true; - } - } - return false; - } private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd, int position) { swipeRefreshLayout.setRefreshing(false); - if(fetchEnd == FetchEnd.MIDDLE && notifications.getPairedItem(position).getType() == Notification.Type.PLACEHOLDER) { - NotificationViewData old = notifications.getPairedItem(position); - NotificationViewData notificationViewData = new NotificationViewData(old.getType(), - old.getId(), old.getAccount(), old.getStatusViewData(), false); - notifications.setPairedItem(position, notificationViewData); - adapter.updateItemWithNotify(position, notificationViewData, true); + if (fetchEnd == FetchEnd.MIDDLE && !notifications.get(position).isRight()) { + NotificationViewData placeholderVD = + new NotificationViewData.Placeholder(false); + notifications.setPairedItem(position, placeholderVD); + adapter.updateItemWithNotify(position, placeholderVD, true); } Log.e(TAG, "Fetch failure: " + exception.getMessage()); fulfillAnyQueuedFetches(fetchEnd); @@ -604,8 +624,8 @@ public class NotificationsFragment extends SFragment implements } } - private void insert(List newNotifications, int pos) { - + private void replacePlaceholderWithNotifications(List newNotifications, int pos) { + // Remove placeholder notifications.remove(pos); if (ListUtils.isEmpty(newNotifications)) { @@ -613,15 +633,29 @@ public class NotificationsFragment extends SFragment implements return; } - if(newNotifications.size() >= LOAD_AT_ONCE) { - Notification placeholder = new Notification(); - placeholder.type = Notification.Type.PLACEHOLDER; - newNotifications.add(placeholder); + List> liftedNew = liftNotificationList(newNotifications); + + // If we fetched less posts than in the limit, it means that the hole is not filled + // If we fetched at least as much it means that there are more posts to load and we should + // insert new placeholder + if (newNotifications.size() >= LOAD_AT_ONCE) { + liftedNew.add(Either.left(Placeholder.getInstance())); } - notifications.addAll(pos, newNotifications); + notifications.addAll(pos, liftedNew); adapter.update(notifications.getPairedCopy()); + } + private final Function> notificationLifter = + new Function>() { + @Override + public Either apply(Notification input) { + return Either.right(input); + } + }; + + private List> liftNotificationList(List list) { + return CollectionUtil.map(list, notificationLifter); } private void fullyRefresh() { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CollectionUtil.java b/app/src/main/java/com/keylesspalace/tusky/util/CollectionUtil.java new file mode 100644 index 000000000..21d4bb081 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/CollectionUtil.java @@ -0,0 +1,38 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ +package com.keylesspalace.tusky.util; + +import android.arch.core.util.Function; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by charlag on 05/11/17. + */ + +public final class CollectionUtil { + private CollectionUtil() { + throw new AssertionError(); + } + + public static List map(List list, Function mapper) { + final List newList = new ArrayList<>(list.size()); + for (E el : list) { + newList.add(mapper.apply(el)); + } + return newList; + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/Either.java b/app/src/main/java/com/keylesspalace/tusky/util/Either.java new file mode 100644 index 000000000..3f134f754 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/Either.java @@ -0,0 +1,125 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ +package com.keylesspalace.tusky.util; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * Created by charlag on 05/11/17. + * + * Class to represent sum type/tagged union/variant/ADT e.t.c. + * It is either Left or Right. + */ +public final class Either { + + /** + * Constructs Left instance of either + * @param left Object to be considered Left + * @param Left type + * @param Right type + * @return new instance of Either which contains left. + */ + public static Either left(L left) { + return new Either<>(left, false); + } + + /** + * Constructs Right instance of either + * @param right Object to be considered Right + * @param Left type + * @param Right type + * @return new instance of Either which contains right. + */ + public static Either right(R right) { + return new Either<>(right, true); + } + + private final Object value; + // we need it because of the types erasure + private boolean isRight; + + private Either(Object value, boolean isRight) { + this.value = value; + this.isRight = isRight; + } + + public boolean isRight() { + return isRight; + } + + /** + * Try to get contained object as a Left or throw an exception. + * @throws AssertionError If contained value is Right + * @return contained value as Right + */ + public @NonNull L getAsLeft() { + if (isRight) { + throw new AssertionError("Tried to get the Either as Left while it is Right"); + } + //noinspection unchecked + return (L) value; + } + + /** + * Try to get contained object as a Right or throw an exception. + * @throws AssertionError If contained value is Left + * @return contained value as Right + */ + public @NonNull R getAsRight() { + if (!isRight) { + throw new AssertionError("Tried to get the Either as Right while it is Left"); + } + //noinspection unchecked + return (R) value; + } + + /** + * Same as {@link #getAsLeft()} but returns {@code null} is the value if Right instead of + * throwing an exception. + * @return contained value as Left or null + */ + public @Nullable L getAsLeftOrNull() { + if (isRight) { + return null; + } + //noinspection unchecked + return (L) value; + } + + /** + * Same as {@link #getAsRightOrNull()} but returns {@code null} is the value if Left instead of + * throwing an exception. + * @return contained value as Right or null + */ + public @Nullable R getAsRightOrNull() { + if (!isRight) { + return null; + } + //noinspection unchecked + return (R) value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof Either)) return false; + Either that = (Either) obj; + return this.isRight == that.isRight && + (this.value == that.value || + this.value != null && this.value.equals(that.value)); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java index a25340d37..8812ed0c7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java @@ -34,7 +34,7 @@ public final class ViewDataUtils { @Nullable public static StatusViewData statusToViewData(@Nullable Status status) { if (status == null) return null; - if(status.placeholder) { + if (status.placeholder) { return new StatusViewData.Builder().setId(status.id) .setPlaceholder(true) .createStatusViewData(); @@ -80,8 +80,8 @@ public final class ViewDataUtils { } public static NotificationViewData notificationToViewData(Notification notification) { - return new NotificationViewData(notification.type, notification.id, notification.account, - statusToViewData(notification.status), false); + return new NotificationViewData.Concrete(notification.type, notification.id, notification.account, + statusToViewData(notification.status)); } public static List notificationListToViewDataList( diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java index 597d390dc..0219f0d6d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java @@ -20,41 +20,59 @@ import com.keylesspalace.tusky.entity.Notification; /** * Created by charlag on 12/07/2017. + * + * Class to represent data required to display either a notification or a placeholder. + * It is either a {@link Placeholder} or a {@link Concrete}. + * It is modelled this way because close relationship between placeholder and concrete notification + * is fine in this case. Placeholder case is not modelled as a type of notification because + * invariants would be violated and because it would model domain incorrectly. It is prefereable to + * {@link com.keylesspalace.tusky.util.Either} because class hierarchy is cheaper, faster and + * more native. */ - -public final class NotificationViewData { - private final Notification.Type type; - private final String id; - private final Account account; - private final StatusViewData statusViewData; - private final boolean placeholderLoading; - - public NotificationViewData(Notification.Type type, String id, Account account, - StatusViewData statusViewData, boolean placeholderLoading) { - this.type = type; - this.id = id; - this.account = account; - this.statusViewData = statusViewData; - this.placeholderLoading = placeholderLoading; +public abstract class NotificationViewData { + private NotificationViewData() { } - public Notification.Type getType() { - return type; + public static final class Concrete extends NotificationViewData { + private final Notification.Type type; + private final String id; + private final Account account; + private final StatusViewData statusViewData; + + public Concrete(Notification.Type type, String id, Account account, + StatusViewData statusViewData) { + this.type = type; + this.id = id; + this.account = account; + this.statusViewData = statusViewData; + } + + public Notification.Type getType() { + return type; + } + + public String getId() { + return id; + } + + public Account getAccount() { + return account; + } + + public StatusViewData getStatusViewData() { + return statusViewData; + } } - public String getId() { - return id; - } + public static final class Placeholder extends NotificationViewData { + private final boolean isLoading; - public Account getAccount() { - return account; - } + public Placeholder(boolean isLoading) { + this.isLoading = isLoading; + } - public StatusViewData getStatusViewData() { - return statusViewData; - } - - public boolean isPlaceholderLoading() { - return placeholderLoading; + public boolean isLoading() { + return isLoading; + } } }