Android Ditches, Part 2: SDKs and Libraries

    When developing for Android, you always need to be alert. A step to the left / a step to the right - and another hour has passed after the debug. Ditches can be anything: from ordinary bugs in the SDK to non-obvious method names with a context-sensitive result (yes, Fragment.getFragmentManager () , it's about me).

    In a previous article , “surface” SDK cuvettes were described that are very easy to get into. This time, the ditches will be deeper, wiser and more specific. There will also be a few points related to Retrofit 2 & Gson .
    image


    1. GridLayout does not respond to layout_weight


    Situation

    Sometimes it happens that the usual way to create an object with a bunch is not suitable:
    Regular form
    image

    Such a situation may be, for example, landscape display of the form. In this case, I would like to have something like this:
    Form for landscape
    image

    How to make the same alignment of 50 to 50? There are several basic approaches :

    However, they all have their drawbacks:
    • The abundance of LinearLayout leads to the monstrousness of xml'ki , and it leads to the death of cats .
    • RelativeLayout complicates the change in the future (swapping several lines in a form or adding a separator will be the same task. I’m completely silent about View.setVisibility (View.GONE ).
    • Well, nobody uses TableLayout at all ... or they use it, but rarely. I don’t know such people.

    Moreover, quite often it is necessary to use the magic of the number 0dp & weight = 1 to achieve a flexible design. Neither TableLayout nor RelativeLayout will help you here. The first time you try to use something like TextView.setEllipsize () , problems and pain will begin.
    And then you probably noticed that I missed another element. It would seem that GridLayout comes to the rescue , but even that is useless because it does not support the layout_weight property . So what to do?

    Decision

    For some time there was really nothing to do - either torment with RelativeLayout , or use LinearLayout , or fill in everything programmatically (for especially perverted ones).
    However, with version 21, GridLayout finally started supporting the layout_weight property and, most importantly, this change was added to AppCompat as android.support.v7.widget.GridLayout !
    By the time I found out about this (and in general that the usual GridLayout wanted to chew on my weight ), I spent at least a week trying to figure out why my layout swam to the right (as here) Perhaps this is one of the most important innovations, which, for some reason, was left without due attention. Fortunately, the answers to stackoverflow ( 1 , 2 ) are already beginning to be added.
    I also advise you to look at the page for the new PercentRelativeLayout and PercentFrameLayout - this is really a bomb. The name speaks for itself and allows you to make an extremely responsive design. iOS’s will appreciate. And oh yes, it is in AppCompat .

    2. Are Fragment.isRemoving () and Acitivity.isFinishing () equal?


    Situation

    Once I wanted to write my PresenterManager as a singleton (greetings from MVP ). To remove Presenter 's in time , I used Activity.isFinishing () , collecting id Presenter 's of fragments in the activity and deleting them with it. Naturally, this method worked poorly in the case of NavigationView - the fragments changed through FragmentTransaction.replace () , Presenter 's accumulated and everything went under the drain.
    Googling the lard , the Fragment.isRemoving () method was found , which seems to do the same, but for fragments. I rewrote the PresenterManager code'and was pleased. The end…

    Decision

    ... my quiet life came when I tried to make it work. Honestly, I tried this way and that, but the behavior of this method is fundamentally different from Activity.isFinishing () . Google was wrong. If you ever have a similar problem, think three times before using Fragment.isRemoving () . I'm serious . Pay particular attention to the logs when you rotate the screen.

    By the way, with Acitivity.isFinishing () , everything is not so smooth either: minimize the application with> 1 activity in the stack, wait for a shortage of memory, go back and use Up Navigation and * voila *! .. It was a simple recipe for how to have an Activity. isFinishing () == falsefor activities that you will never see again.
    UPD : As the bejibx user correctly noted , the remark about Activity.isFinishing () is not entirely true. To understand why, it is better to read the comment thread .

    3. Header / Footer in RecyclerView


    Situation

    A common task when implementing pagination is to display the ProgressBar while new data is loading.
    Unlike ListView , RecyclerView has much more features - what is it worth of RecyclerView.Adapter.notifyItemRangeInserted () compared to the very headache of ListView .
    However, when you try to use it in the project instead of ListView , you immediately come across a lot of nuances: where is the property ListView.setDivider () ? Where is something like ListView.addHeaderView () ? What else is RecyclerView.Adapter.getItemViewType () , etc., etc.
    It’s not difficult to deal with all this dump of new information, but something unpleasant still remains. Adding Divider / Header requires writing clouds of code. What can we say about complex layouts ? Dovecha ever do RecyclerView with 4 different the Header 's and the Footer ' th with Controllers. Let's just say, saddened and dejected, I walked for a very long time.

    Decision

    In fact, everything is not so bad if you know what to look for. The main problem of RecyclerView (and its main advantage) is that you can do anything with it. There is virtually no scope. From here the problem follows: if you want Header - do it yourself. But fortunately, “do it yourself” others have already done for us, so let's use it.
    Typical problems and their solutions:
    • Headings for groups of elements (for example, in the dictionary "A" will be the heading for all words starting with this letter) - the easiest way to do this is through a single item-layout without adding a second unnecessary ViewHolder type . Add a check that the current element will mark the transition from one letter to another and include the title hidden in the layout through View.VISIBLE .
    • Simple divider - copy and paste this code into the project. No unnecessary fraud. Works through RecyclerView.addItemDecoration ()
    • Add Header / Footer / Drag & Drop etc. - if you do with pens, then either start a new type for each new ViewHolder (I do not recommend it), or do a WrapperAdapter (much nicer). But it's even better to look here and choose your favorite lib. Personally, I like two at once: FastAdapter and UltimateRecyclerView
    • Pagination is needed , but too lazy to mess with Header / Footer for ProgressBars - the Paginate library from one of the Twitter developers.

    Although for me it still remains a mystery - why it was impossible to make any SimpleDivider / SimpleHeaderAdapter , etc. directly to the SDK?

    4. Acceleration with RecyclerView.Adapter.setHasStableIds ()


    What is wrong with him?

    Not as much a problem as a lack of documentation . Here is what it says:
    Returns true if this adapter publishes a unique long value that can act as a key for the item at a given position in the data set. If that item is relocated in the data set, the ID returned for that item should be the same.

    And here people are divided into two types. First: everything is clear. Second: what does it mean then?
    The problem is that even if you referred yourself to the first people, you may be confused by the question: why this method? Yes, to return a unique ID ! I know. But why is it needed? And no, the answer “Google writes that it will scroll so faster!” Will not suit me.

    And here's the thing

    Acceleration from RecyclerView.Adapter.setHasStableIds () can really be obtained, but only in one case - if you use RecyclerView.Adapter.notifyDataSetChanged () everywhere (and here they deigned to write why stable id is needed) . If you have static data, then this method will not give you anything, and maybe even slow down a bit due to internal ID checks . I only found out about this after reading the source, and a bit later I stumbled upon this article .

    5. WebView


    Situation

    The task is to get the html text from the server and display it on the screen. The text is given by the server as "& lt; html & gt;" . All. This is the whole task. Difficult? There is a WebView that can display html in a couple of lines. What is it, even TextView can do it! One or two and you're done ... yes? .. no? .. well, should it ?!

    Decision

    Unfortunately, everything is not so smooth here:
    • To begin with, there is no method like HtmlUtils.unescape () in the Android SDK. If you want & lt; turn into "<" , then the easiest way (besides prescribing regex 'with pens) is to connect apache with its StringUtils.unescapeHtml4 () .
    • The next problem will be artifacts when scrolling. Quite suddenly (yes, the Android SDK?), The WebView will blink in black. What to do is explained here and here . Personally, only a combination of these approaches helped me.
    • And if you still haven’t been surprised by the abundance of problems from such a simple task, then here's the finish: you need to display the ProgressBar before the html page renders. And here everything is bad. That is really bad. All solutions presented on stackoverflow work every other time or do not work at all ( tyk , tyk ). The only hitherto working method was using WebView.setPictureListener () , however, it has now been declared deprecated and there's nothing to be done about it.
      As a result, the only thing that can be advised is to abandon the ProgressBar . Or, if you really, really, really want to - add it directly tohtml- code, checking through javascript the readiness of the page. But this is already for the club of elite Mazahists.


    6. Gson : a bitmask as an EnumSet


    When / Where / Why?

    (The situation is specific and does not apply directly to Android’s problems, but decided to add as a seed before the next cuvette)
    In response to one of the api requests, a bit mask of access rights in the form of an int comes in . It is necessary to process the elements of this mask.
    The first thing that comes to mind is int 's constants and bitwise operations for checks. Sure, it always works. But what if you want more? What about EnumSet ?
    “No problem” - the progger-bearded man will answer and break the architecture of models into several levels: POJO, Model, Entity, UiModel and what the hell is not joking. But if you are too lazy and want without extra. classes? What then?

    Decision

    We create the enum we need, taking care of the “bit-like” names in @SerializedName :
    enum access
    public enum Access {
        @SerializedName("1")
        CREATE,
        @SerializedName("2")
        READ;
        @SerializedName("4")
        UPDATE;
        @SerializedName("8")
        DELETE;
    }
    


    Define a JsonDeserializer for deserialization from json to EnumSet :
    EnumMaskConverter
    public class EnumMaskConverter> implements JsonDeserializer> {
    	Class enumClass;
    	public EnumMaskConverter(Class enumClass) {
    		this.enumClass = enumClass;
    	}
    	@Override
    	public EnumSet deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
    		long mask = json.getAsLong();
    		EnumSet set = EnumSet.noneOf(enumClass);
    		for (E bit : enumClass.getEnumConstants()) {
    			final String value = EnumUtils.GetSerializedNameValue(bit);
    			assert value != null;
    			long key = Integer.valueOf(value);
    			if ((mask & key) != 0) {
    				set.add(bit);
    			}
    		}
    		return set;
    	}
    }
    


    And add it to Gson :
    Gsonbuilder
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeAdapter((new TypeToken>() {}).getType(), new EnumMaskConverter<>(Access.class));
    Gson gson = gsonBuilder.create();
    


    As a result:
    Using
    class MyModel {
            @SerializedName("mask")
    	public EnumSet access;
    }
    /* ...some lines later... */
    if (myModel.access.containsAll(EnumSet.of(Access.READ, Access.UPDATE, Access.DELETE))) { 
    	/* do something really cool */ 
    }
    



    7. Retrofit : Enum in @GET request


    Situation

    Let's start with the setup. Gson is formed as before. Retrofit is created like this:
    new Retrofit.Builder ()
    retrofit = new Retrofit.Builder()
                    .baseUrl(ApiConstants.API_ENDPOINT)
                    .client(httpClient)
                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                    .build();
    


    And the data looks like this:
    enum season
    public enum Season {
        @SerializedName("3")
        AUTUMN,
        @SerializedName("1")
        SPRING;
    }
    


    Thanks to Gson's ability to directly enum parsing through regular @SerializedName , it became possible to get rid of the need to create all sorts of additional layer classes. All data will immediately go from requests to Model . Everything is fine:
    Retrofit service
    public interface MonthApi {
            @GET("index.php?page[api]=selectors")
            Observable getPriorityMonthSelector();
            @GET("index.php?page[api]=years")
            Observable getFirstMonth(@Query("season") Season season);
    }
    


    Application
    class MonthSelector {
            @SerializedName("season")
    	public Season season;
    }
    /* ...some mouses later... */
    MonthSelector selector = monthApi.getPriorityMonthSelector();
    Season season = selector.season;
    /* ...some cats later... */
    Month month = monthApi.getFirstMonth(season);
    


    And now, dear experts, attention is a question! What went wrong and why does it not work?

    Decision

    I specifically omitted the information that this is not working here. The fact is, if you look at the logs, then the monthApi.getFirstMonth (season) request will be processed as index.php? Page [api] = years & season_lookup = AUTUMN ... "uh, what are you doing?" - I will say. What is your answer? Why such a result? Have not guessed yet? Then you hit.
    When I came across this task, it took me several hours of searching in the source to understand one thing (or rather even remember): yes, Gson is not used when sending @GET / @POST and other similar _ requests_ in general! Indeed, when was the last time you saw something like index.php? Page [api] = years & season_lookup = {a: 123; b: 321}? It does not make sense. Retrofit 2 uses Gson only when converting Body , but not for the requests themselves. Eventually? just season.toString () is used - hence the result.
    However, if you really want (and I am one of them) to use enum with conversion through Gson in the request, then here you have another converter, everything is as always.

    8. Retrofit : auth-token transfer


    And finally, I would like to say one thing to those who write like this:
    Any Retrofit Service
    public interface CoolApi {
        @GET("index.php?page[api]=need")
        Observable 
        just(@Header("auth-token") String authToken);
        //           ^шлём auth-token
        @GET("index.php?page[api]=more")
        Observable 
        not(@Header("auth-token") String authToken);
        //           ^шлём auth-token ещё раз
        @GET("index.php?page[api]=gold")
        Observable 
        doIt(@Header("auth-token") String authToken);
        //           ^шлём auth-token в 101ый раз!
    }
    


    Start using Interceptor 's already ! I understand that Retrofit is very simple to use and therefore no one reads the documentation, but when you sit for 3 hours and clean out the code not only from auth-token , but also from all sorts of specific current_location, battery_level, busy_status - a great sadness catches up (do not ask why to transfer battery_level in every request. It’s in shock). You can read about it here .

    Instead of a conclusion


    Well, this time there was a lot more text than I planned. Some less interesting cuvettes had to be thrown out, while others I decided to leave for the next time.
    Contrary to the previous part , this time I tried to make you not “google in the first place,” but first of all think “why am I doing this?” Sometimes the problem is not created by the SDK or library, but by the programmer himself and, unfortunately, in this case everything is much worse. Do not underestimate the selected tools, as well as do not overestimate it.
    In general, if you like the android and / or you plan to do it, always keep yourself in the know about global trends . Well, or look here for a more convenient news resource. There you can find a lot of information aboutAndroid SDK , popular libraries , etc., etc.

    Also popular now: