📜 ⬆️ ⬇️

Google leanback - big brother care

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:
')

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.


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:


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:


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:


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.

Example


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:

 /** * Layout logo position based on current state. Subclass may override. * The method is called when a logo is bound to view or state changes. * @param viewHolder The row ViewHolder that contains the logo. * @param oldState The old state, can be same as current viewHolder.getState() * @param logoChanged Whether logo was changed. */ 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); } } 

Result


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:


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.

Source: https://habr.com/ru/post/351084/


All Articles