Good afternoon, readers of Habr. I would like to share my experience of developing an application for Android TV on the example of DetailsFragment.
There are official examples
here and official documentation
here . What made me express my opinion? This is what official examples do not meet modern development requirements, for example, modularity and extensibility. Sometimes there is a certain duality when using a particular mechanism. Consider in more detail DetailsFragment.
In order to start developing your application for the android platform, in my opinion, you should accept 2 basic truths:
')
- A bad idea to move away from official recommendations and develop a customized application. Google took care to make it extremely difficult.
- Single Activity Architecture is also not quite suitable, it is fraught with memory leaks associated with the internal implementation of the leanback library.
So, first things first:
Briefly about the library Leanback
The Leanback library is a set of screen templates with various functional features. There are screens for displaying lists, content cards, dialogs, etc. These screens handle all custom transitions between elements and animations, and also have quite extensive functionality for building simple out-of-box applications. The ideology of this library is that all applications based on it should be similar in terms of use. My personal opinion is a pretty good idea to create a uniform application in the market. I don't need to think anymore, will the user know what can be scrolled down? He learns, because he has already used hundreds of similar applications.
But, as in a large number of libraries connected to the project, there are sharp differences between the customer’s expectations of the product and the capabilities of this library. In one example, I will try to make two main conclusions that I have drawn for myself by developing this application.
So, the DetailsFragment class
This class serves to display the “content card”. Content cards are a screen space that displays complete information about a particular entity object. In most cases, when a user clicks something in the list of similar entity objects, he hits exactly on the content card.
What is DetailsFragment?
This class is a collective image of custom views. The logical structure is as follows:

We will understand in order what element is engaged in.
- ArrayObjectAdapter is a class that collects all the elements on the screen.
- DetailsOverviewRow - part of the main adapter, responsible for the display of functional elements (Actions), for information (DescriptionView) and the image of the content card.
- Additional Row - this series includes additional elements that extend the functionality of the content card.
And so at first glance, we all just have a certain “matryoshka” in which the roles are clearly distributed (in fact, not, then we will see this for ourselves).
I’ll dwell only on the basic concepts of DetailsOverviewRow, because in my opinion they are really quite interesting.
The DetailsOverviewRow class has the following main methods:
- setImageDrawable (Drawable drawable) - this method sets the “avatar” of our content card. An alternative method for setting avatars may be setImageBitmap (Context context, Bitmap bm).
- void setActionsAdapter (ObjectAdapter adapter) - setting the adapter for content card events (for example, buy / watch / add to favorites and so on). ObjectAdapter is an abstract class. In leanback there are several implementations on classic structures (for example, ArrayObjectAdapter). We can add different classes to our ObjectAdapter, in this case we can resort to the standard Action class.
- To install DescriptionView, the constructor of the DetailsOverviewRow class is used, which accepts a certain model.
I wonder how DetailsOverviewRow puts our model with information to display? Where does the markup for this display come from?
Presenter (not classic MVP)
It so happened that Google called the class, which is responsible for how this or that model looks like within the framework of the internal view, presenter. Next, for convenience, I will call it a UI presenter.
So, UI presenters, this is essentially how our data objects or entity objects get on the screen. If we draw an analogy with the classic android development, the adapter does it all.
In the case of DescriptionView, we need to create a ui presenter that will put the model on a given custom view. There are 2 main ways we can do this:
- FullWidthDetailsOverviewRowPresenter - this class is derived from RowPresenter. It is a full-screen DetailsOverviewRow display. Appearance:
- DetailsOverviewRowPresenter is no longer supported. Displayed as in the picture below.
As a result, we choose FullWidthDetailsOverviewRowPresenter, since there are no alternatives out of the box.
Creating a ui presenter will look like this:
FullWidthDetailsOverviewRowPresenter rowPresenter = new FullWidthDetailsOverviewRowPresenter( new DetailsDescriptionPresenter(context))
DetailsDescriptionPresenter is a class that extends from Presenter. Responsible for the custom mapping of the entity object (most often it contains the name and description).
As mentioned earlier, ui presenter is an analogue of the adapter in classic android. The following methods are required for implementation:
- ViewHolder onCreateViewHolder (ViewGroup parent) - this method is designed to create a ViewHolder object. Here we can create our custom view and pass it to the ViewHolder.
- void onBindViewHolder (ViewHolder viewHolder, Object item) is a method for constructing our ViewHolder. As you can see, there is a little slippery situation here, since the data object is transferred as a java object. You can get a runtime error if you use the downward transform incorrectly.
- void onUnbindViewHolder (ViewHolder viewHolder) - this method serves to free our holder from resources so that the garbage collector can safely remove it.
Overall picture
Throughout the android TV project, you will use an ArrayObjectAdapter with custom presentations, you may be using factories presenters. It’s worth remembering that they simply invest in each other and in the implementation of a specific screen they give some form of presentation. For example, I created my own descendant class ui presenter, called it AbstractCardPresenter. This class has rescued me more than once as it smoothes bumps with the transformations at the level of their appearance. Also created a basic representation of the cards. This helped me reuse ready-made views where they are needed and partially customize the cards.
AbstractCardPresenter public abstract class AbstractCardPresenter<T extends BaseCardView> extends Presenter { private static final String TAG = "AbstractCardPresenter"; private final Context mContext; public AbstractCardPresenter(Context context) { mContext = context; } public Context getContext() { return mContext; } @Override public final ViewHolder onCreateViewHolder(ViewGroup parent) { T cardView = onCreateView(); return new ViewHolder(cardView); } @Override public final void onBindViewHolder(ViewHolder viewHolder, Object item) { Card card = (Card) item; onBindViewHolder(card, (T) viewHolder.view); } @Override public final void onUnbindViewHolder(ViewHolder viewHolder) { onUnbindViewHolder((T) viewHolder.view); } public void onUnbindViewHolder(T cardView) { } protected abstract T onCreateView(); public abstract void onBindViewHolder(Card card, T cardView); }
“Bad idea to move away from official recommendations”?
It is bad because in the classes that were carefully written for us, most of the methods are immutable for the simple reason of strong inner connectedness. In order not to disturb the internal state of the screen (in fact, DetailsFragment and others are fragments), you should use them as intended. I will not go into the details of the implementation of the internal classes, the state machine and other ideas of the developers of this library. A real example from my work is the leakage of a fragment using the Single Activity Architecture.
This leak was associated with DetailsFragment transitions. Through trial and error, we managed to find the cause of the leak, fix the leak and write a report to the bug. Given the low power of the TVs themselves (Sony Brawia 4K 2GB RAM), the OOM problem is quite acute. Leakage is eliminated by zeroing these transitions. When using transitions between activations, this problem was not observed.
TransitionHelper.setReturnTransition(getActivity().getWindow(), null); TransitionHelper.setEnterTransition(getActivity().getWindow(), null);
Out of the box does not work!
If you really want (requires the customer) to change this or that display, you can do this, I will tell you with an example I encountered. For my development experience under android tv, I have seen many constraints: it is impossible to track the internal fragments created by the library; their life cycle is not controlled by anyone; calls to create custom views in constructors (asynchronous data cannot be used). Google did almost everything so that it was impossible to write “how to”. Taking into account modern requests, a non-flexible mechanism turns out to be bad and not needed, but since there are no alternatives (apart from writing your own leanback), you have to live with what we have.
The first thing that drew my attention to the implementation of the box is the avatar of the content card. When you switch the focus up and down, it is absolutely non-animated twitching down.
Having narrowed down the search for classes that are responsible for this view, I went to the implementation of the FullWidthDetailsOverviewRowPresenter class to find the answer to the question of how it moves. I managed to find a method that is responsible for moving the avatar of our content card - void onLayoutLogo (ViewHolder viewHolder, int oldState, boolean logoChanged).
The default implementation was as follows:
protected void onLayoutLogo(ViewHolder viewHolder, int oldState, boolean logoChanged) { View v = viewHolder.getLogoViewHolder().view; ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); switch (mAlignmentMode) { case ALIGN_MODE_START: default: lp.setMarginStart(v.getResources().getDimensionPixelSize( R.dimen.lb_details_v2_logo_margin_start)); break; case ALIGN_MODE_MIDDLE: lp.setMarginStart(v.getResources().getDimensionPixelSize(R.dimen.lb_details_v2_left) - lp.width); break; } switch (viewHolder.getState()) { case STATE_FULL: default: lp.topMargin = v.getResources().getDimensionPixelSize(R.dimen.lb_details_v2_blank_height) - lp.height / 2; break; case STATE_HALF: lp.topMargin = v.getResources().getDimensionPixelSize( R.dimen.lb_details_v2_blank_height) + v.getResources() .getDimensionPixelSize(R.dimen.lb_details_v2_actions_height) + v .getResources().getDimensionPixelSize( R.dimen.lb_details_v2_description_margin_top); break; case STATE_SMALL: lp.topMargin = 0; break; } v.setLayoutParams(lp); }
The implementation was found, then I created a descendant class FullWidthDetailsOverviewRowPresenter in which I redefined the onLayoutLogo method and wrote my implementation.
public class CustomMovieDetailsPresenter extends FullWidthDetailsOverviewRowPresenter { private int mPreviousState = STATE_FULL; public CustomMovieDetailsPresenter(final Presenter detailsPresenter) { super(detailsPresenter); setInitialState(FullWidthDetailsOverviewRowPresenter.STATE_FULL); } @Override protected void onLayoutLogo(final ViewHolder viewHolder, final int oldState, final boolean logoChanged) { final View v = viewHolder.getLogoViewHolder().view; final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); lp.setMarginStart(v.getResources().getDimensionPixelSize( android.support.v17.leanback.R.dimen.lb_details_v2_logo_margin_start)); lp.topMargin = v.getResources().getDimensionPixelSize(android.support.v17.leanback.R.dimen.lb_details_v2_blank_height) - lp.height / 2; switch (viewHolder.getState()) { case STATE_FULL: default: if (mPreviousState == STATE_HALF) { v.animate().translationY(0); } break; case STATE_HALF: if (mPreviousState == STATE_FULL) { final float offset = v.getResources().getDimensionPixelSize(android.support.v17.leanback.R.dimen.lb_details_v2_actions_height) + v .getResources().getDimensionPixelSize(android.support.v17.leanback.R.dimen.lb_details_v2_description_margin_top)+lp.height/2; v.animate().translationY(offset); } break; } mPreviousState = viewHolder.getState(); v.setLayoutParams(lp); } }
In this case, everything was decided relatively simply, with the state when the screen goes down, we animatedly descend behind it and the avatar of the content card. When returning to the starting position, the animated avatar is also animated upwards. But there are cases that the method is declared final or is inaccessible, then I resorted to reflection. Reflection is an extreme stage, as it has several disadvantages:
- When updating the library, it may happen that the field to which we accessed through reflection is renamed, or even worse, is deleted.
- Reflection is quite a resource-intensive operation, which often causes difficulties in the operating system
- Reflection is difficult to read and difficult to maintain.
In other words, reflection is an extreme instance, to which I resorted once. But how - I remember the mechanism.
A little bit about the multi-layered architecture in android tv application
In this case, everything is relatively simple, problems can arise only in the layer of user views, since it is sometimes difficult to understand exactly where this or that element belongs. Returning to our example with DetailsFragment, the real tasks will be something like the following: If the content is purchased, display the “Watch” button; If the content is rented, then display the watch button + rental end time, etc .; With all this, there is a button trailer, a button to add to favorites, etc. In my opinion, the presenter (MVP) should receive some kind of model and call the addAction method (android.support.v17.leanback.widget.Action action). That is, based on the data, the presenter concludes which buttons should be added, generates them and invokes the corresponding external interface method with a view. This is where the problem of dependency presenter from the library leanback. Since, for good, you need to use this presenter in other parts of our program, for example, on a mobile device, the problem arises rather sharply. Thus, I introduced a rule for developing presenters in a project in which I participate - not to declare implicit dependencies in the presenter that are tied to the framework.
To avoid this, it was decided in the presenters to use a local analogue of android.support.v17.leanback.widget.Action. This solved a lot of problems in the presenter, but it gave rise to a twofold logic in the twist associated with processing the position of adding and handling pressure, since in the twist we can easily operate with widgets provided by leanback. The same twofold logic appears when the set of buttons is initially unknown, but they have certain priorities. For example, the “watch” button should be in front of the trailer button, the buy button should be after the trailer, and so on. Accordingly, a certain mechanism appears in the view that matches the identifiers of the buttons and their positions, which makes an “priority to display” from the identifier. I avoided this situation quite trivially, but again with the twist I am beginning to acquire logic and know that this is not just an identifier.
private final List<Integer> mActionsIndexesList = new ArrayList<>(); @Override public void addAction(final MovieDetailAction movieDetailAction) { final Action action = new Action(movieDetailAction.getId(), movieDetailAction.getTitle(), movieDetailAction.getSubTitle(), movieDetailAction.getIcon()); actionAdapter.set(movieDetailAction.getId(), action); mActionsIndexesList.add(movieDetailAction.getId()); Collections.sort(mActionsIndexesList); } @Override public void setSelectedAction(final int actionId) { new Handler().postDelayed(() -> mActionsGridView.smoothScrollToPosition(getActionPositionByActionId(actionId)), 100); } private int getActionPosition(final int actionId) { return mActionsIndexesList.indexOf(actionId); }
In custody
Android Tv application development is relatively new, and therefore interesting. At the time of this writing, the Android Tv community is decentralized and therefore most of the problems are solved by “treading their tracks”. Also, in my opinion, programming within limited resources (RAM, computational power, etc.) is quite interesting. Such a pattern of thinking is not always peculiar to developers of classic android applications and in my opinion is a useful experience.