📜 ⬆️ ⬇️

Android Cuvettes, Part 1: SDK

For quite a long time, I could not understand what the difference between a “library” and a “framework” was. No, no, I knew how to read and google, but I still could not understand the meaning of these concepts. Having started programming for android, I finally understood what the words "the programmer uses library, but the programmer uses the framework".
In this series of articles, I want to talk about the problems with which I had to be filled when developing for android. My goal is not to provide any uber solutions to these problems, but only to inform you about the problems that might be encountered by someone who encroaches on the holy grail of the Android SDK. I do not think that the harsh signors will discover America for themselves, but as they say: “repetition is the mother of learning”.
image


1. Dismiss for DatePickerDialog calls OnDateSetListener handler


Situation

A rather unpleasant problem for a beginner. Especially if you expect the SDK to work like a clock.
In due time it was necessary to tinker to understand what was the matter. The problem was aggravated by the fact that after setting the time there was no feedback in the application (the new time was not displayed on the screen). All data was immediately entered into an object that was saved in the database and after several screens was read back.
It is easy to imagine where debugs started from - from the display screen (because Date.now () was used for tests, this added an extra embarrassment), and then by chain.

Decision

In fact, the Lollipop bug was fixed , but who does this suit? Google does not plan to add Google fix to AppCompat , so a workaround is needed. And he is - copied the entire file entirely and carry. Information about the implementation can be read on stackoverflow .
Datepickerdialogfragment
/* * Copyright 2012 David Cesarino de Sousa * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.davidcesarino.android.common.ui; import android.annotation.TargetApi; import android.app.Activity; import android.app.DatePickerDialog; import android.app.DatePickerDialog.OnDateSetListener; import android.app.Dialog; import android.content.DialogInterface; import android.os.Build; import android.os.Bundle; import android.support.v4.app.DialogFragment; import android.widget.DatePicker; /** * <p>Provides a usable {@link DatePickerDialog} wrapped as a {@link DialogFragment}, * using the compatibility package v4. Its main advantage is handling Issue 34833 * automatically for you.</p> * * <p>Current implementation (because I wanted that way =) ):</p> * * <ul> * <li>Only two buttons, a {@code BUTTON_POSITIVE} and a {@code BUTTON_NEGATIVE}. * <li>Buttons labeled from {@code android.R.string.ok} and {@code android.R.string.cancel}. * </ul> * * <p><strong>Usage sample:</strong></p> * * <pre>class YourActivity extends Activity implements OnDateSetListener * * // ... * * Bundle b = new Bundle(); * b.putInt(DatePickerDialogFragment.YEAR, 2012); * b.putInt(DatePickerDialogFragment.MONTH, 6); * b.putInt(DatePickerDialogFragment.DATE, 17); * DialogFragment picker = new DatePickerDialogFragment(); * picker.setArguments(b); * picker.show(getActivity().getSupportFragmentManager(), "fragment_date_picker");</pre> * * @author davidcesarino@gmail.com * @version 2015.0904 * @see <a href="http://code.google.com/p/android/issues/detail?id=34833">Android Issue 34833</a> * @see <a href="http://stackoverflow.com/q/11444238/489607" * >Jelly Bean DatePickerDialog — is there a way to cancel?</a> * */ public class DatePickerDialogFragment extends DialogFragment { public static final String YEAR = "Year"; public static final String MONTH = "Month"; public static final String DATE = "Day"; private OnDateSetListener mListener; @Override public void onAttach(Activity activity) { super.onAttach(activity); this.mListener = (OnDateSetListener) activity; } @Override public void onDetach() { this.mListener = null; super.onDetach(); } @TargetApi(11) @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Bundle b = getArguments(); int y = b.getInt(YEAR); int m = b.getInt(MONTH); int d = b.getInt(DATE); // Jelly Bean introduced a bug in DatePickerDialog (and possibly // TimePickerDialog as well), and one of the possible solutions is // to postpone the creation of both the listener and the BUTTON_* . // // Passing a null here won't harm because DatePickerDialog checks for a null // whenever it reads the listener that was passed here. >>> This seems to be // true down to 1.5 / API 3, up to 4.1.1 / API 16. <<< No worries. For now. // // See my own question and answer, and details I included for the issue: // // http://stackoverflow.com/a/11493752/489607 // http://code.google.com/p/android/issues/detail?id=34833 // // Of course, suggestions welcome. final DatePickerDialog picker = new DatePickerDialog(getActivity(), getConstructorListener(), y, m, d); if (isAffectedVersion()) { picker.setButton(DialogInterface.BUTTON_POSITIVE, getActivity().getString(android.R.string.ok), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { DatePicker dp = picker.getDatePicker(); mListener.onDateSet(dp, dp.getYear(), dp.getMonth(), dp.getDayOfMonth()); } }); picker.setButton(DialogInterface.BUTTON_NEGATIVE, getActivity().getString(android.R.string.cancel), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) {} }); } return picker; } private static boolean isAffectedVersion() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP; } private OnDateSetListener getConstructorListener() { return isAffectedVersion() ? null : mListener; } } 


')

2. Button requires double click before it works.


Situation

Another case from the category of "yes, as it is so!". Imagine a situation where, in an open dialog box, you fill in the data in several EditText 's, and then click OK. What can go wrong? Well, for example, the OK button can ignore your click! But only the first ... and not always ... and only once a month .... And of course, debagging and debugging begins again, and debugging ...

Decision

And the decision is utterly simple. You need to know about setFocusableInTouchMode () magic and what this is all about. Many of us do not pay enough attention to the focusable property. Indeed, it feels on its own and in most cases behaves as it should. And here they catch us:

Simply? It was not there, there are always exceptions. In some cases, the focus may be present in this mode (which, I repeat, is called the “non-focus mode”). The most striking example is EditText . This behavior is necessary for the simultaneous interaction of the keyboard and EditText as well. Otherwise, to write something did not work.
As a result, the solution is focusableInTouchMode = true for the button. The solution looks simple, but when you do not know where to start, everything acquires other colors. More details can be read on android-developers.blogspot.ru .

3. Bundle.putParcelable () - not always serialization


Situation

There is a dialog box, there is an activation. You, as a brave comrade, decide to transfer your object of the VeryComplexModel class to the dialog box to do some actions (eg, editing) on ​​it, and then return it back in order to activate the new version in the database.
And again, magic happens during the closing of the dialogue. It would seem that the local copy of the object should remain old, but no. She has changed.

Decision

It's all about the misrepresentation of mechanisms in the Bundle . In my understanding, Bundle , like any Serializable and JSONObject , always creates an object from scratch, if you sequentially serialize () and deserialize () . Anyway, I thought so. However, whether for considerations of optimization, or for some other reason, the Bundle can carry a pointer to your object without serialization. From here and change of the data in a dialog box despite dismiss . It was expected that only a copy would suffer, but the Android SDK ordered otherwise.

4. getFragmentManager () inside the fragment


Situation

Perhaps this is the most common problem among novice (and not only!) Programmers. It is necessary to relax a bit and an hour or two debug is guaranteed.
FragmentManager is used to manage the fragments inside the activation, as well as to manage the nested fragments inside other fragments.
Activation has a getFragmentManager () method, a fragment has a getFragmentManger () method - called, used, works ... or not? .. Something broke again. SDK, take pity!

Decision

Unfortunately, two things play a cruel joke:

If you look at the documentation, you immediately see that getFragmentManager () for a fragment returns ... Parent's FragmentManager ! To get a normal, working as expected FragmentManager, you must use getChildFragmentManager () .

5. Drawable modification at runtime


Situation

I had a chance to deal with this problem during the creation of multi-colored backgrounds for various objects. You can imagine this by the example of a chat, where the bubble (message bubble) for your message is gray, and for the interlocutor in red.
Of course, the easiest solution would be to create 2 independent resources. But what if this is your home project “on time”, and the artist of you is like a ballet dancer? Here various methods of the type setColorFilter () come to the rescue. Right? .. No.

Decision

Just by taking and applying setColorFilter () on any R.drawable.bg_bubble , a change will be made on all bg_bubble in the area in the project.
The fact is that if a user sees 100 messages with bg_bubble , this does not mean that there are 100 copies of this resource. It just does not make sense. For optimization purposes, only one copy is stored and therefore the changes in bg_ bubble will affect all messages at once.
The simplest solution is to create a local copy :
 Drawable clone = drawable.getConstantState().newDrawable(); 

In more detail the essence of the problem is described here on another example .

6. Aligning a TextView with a TextView , regardless of different sizes / fonts


What? Understood nothing
image

Without further ado, immediately link - Watch That Baseline Alignment . And even two .
There is nothing to say about the baseline , but only when you know that it exists. But if you do not know ... then the fun begins with padding & margin . I personally saw this. Even the "beast" of such a code can not be called a language.

7. Spinner without default value


Situation

There is a Spinner for which you need to add "protection against a fool" in the form of a hint, which is not the value of the spinner itself.
Spinner with hint
image

You might think that the Spinner.setPrompt () method does the job, but it was not there . It only works for dialog boxes, and not on all versions of the android is displayed. What to do?

Decision

“Nothing. Live with it ”(c) Android SDK.
As usual, you need a hack. The first thing that comes to mind: add 1 element with a description in the beginning of the array. However, this is a bad decision. Not only the “hint” can now be selected as the Spinner's value, but also the problems begin when using the R.array / CursorAdapter .
And as always, the best source of hacks on stackoverflow .
NothingSelectedSpinnerAdapter
 import android.content.Context; import android.database.DataSetObserver; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ListAdapter; import android.widget.SpinnerAdapter; /** * Decorator Adapter to allow a Spinner to show a 'Nothing Selected...' initially * displayed instead of the first choice in the Adapter. */ public class NothingSelectedSpinnerAdapter implements SpinnerAdapter, ListAdapter { protected static final int EXTRA = 1; protected SpinnerAdapter adapter; protected Context context; protected int nothingSelectedLayout; protected int nothingSelectedDropdownLayout; protected LayoutInflater layoutInflater; /** * Use this constructor to have NO 'Select One...' item, instead use * the standard prompt or nothing at all. * @param spinnerAdapter wrapped Adapter. * @param nothingSelectedLayout layout for nothing selected, perhaps * you want text grayed out like a prompt... * @param context */ public NothingSelectedSpinnerAdapter( SpinnerAdapter spinnerAdapter, int nothingSelectedLayout, Context context) { this(spinnerAdapter, nothingSelectedLayout, -1, context); } /** * Use this constructor to Define your 'Select One...' layout as the first * row in the returned choices. * If you do this, you probably don't want a prompt on your spinner or it'll * have two 'Select' rows. * @param spinnerAdapter wrapped Adapter. Should probably return false for isEnabled(0) * @param nothingSelectedLayout layout for nothing selected, perhaps you want * text grayed out like a prompt... * @param nothingSelectedDropdownLayout layout for your 'Select an Item...' in * the dropdown. * @param context */ public NothingSelectedSpinnerAdapter(SpinnerAdapter spinnerAdapter, int nothingSelectedLayout, int nothingSelectedDropdownLayout, Context context) { this.adapter = spinnerAdapter; this.context = context; this.nothingSelectedLayout = nothingSelectedLayout; this.nothingSelectedDropdownLayout = nothingSelectedDropdownLayout; layoutInflater = LayoutInflater.from(context); } @Override public final View getView(int position, View convertView, ViewGroup parent) { // This provides the View for the Selected Item in the Spinner, not // the dropdown (unless dropdownView is not set). if (position == 0) { return getNothingSelectedView(parent); } return adapter.getView(position - EXTRA, null, parent); // Could re-use // the convertView if possible. } /** * View to show in Spinner with Nothing Selected * Override this to do something dynamic... eg "37 Options Found" * @param parent * @return */ protected View getNothingSelectedView(ViewGroup parent) { return layoutInflater.inflate(nothingSelectedLayout, parent, false); } @Override public View getDropDownView(int position, View convertView, ViewGroup parent) { // Android BUG! http://code.google.com/p/android/issues/detail?id=17128 - // Spinner does not support multiple view types if (position == 0) { return nothingSelectedDropdownLayout == -1 ? new View(context) : getNothingSelectedDropdownView(parent); } // Could re-use the convertView if possible, use setTag... return adapter.getDropDownView(position - EXTRA, null, parent); } /** * Override this to do something dynamic... For example, "Pick your favorite * of these 37". * @param parent * @return */ protected View getNothingSelectedDropdownView(ViewGroup parent) { return layoutInflater.inflate(nothingSelectedDropdownLayout, parent, false); } @Override public int getCount() { int count = adapter.getCount(); return count == 0 ? 0 : count + EXTRA; } @Override public Object getItem(int position) { return position == 0 ? null : adapter.getItem(position - EXTRA); } @Override public int getItemViewType(int position) { return 0; } @Override public int getViewTypeCount() { return 1; } @Override public long getItemId(int position) { return position >= EXTRA ? adapter.getItemId(position - EXTRA) : position - EXTRA; } @Override public boolean hasStableIds() { return adapter.hasStableIds(); } @Override public boolean isEmpty() { return adapter.isEmpty(); } @Override public void registerDataSetObserver(DataSetObserver observer) { adapter.registerDataSetObserver(observer); } @Override public void unregisterDataSetObserver(DataSetObserver observer) { adapter.unregisterDataSetObserver(observer); } @Override public boolean areAllItemsEnabled() { return false; } @Override public boolean isEnabled(int position) { return position != 0; // Don't allow the 'nothing selected' // item to be picked. } } 


Usage example
 Spinner spinner = (Spinner) findViewById(R.id.spinner); ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.planets_array, android.R.layout.simple_spinner_item); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setPrompt("Select your favorite Planet!"); spinner.setAdapter( new NothingSelectedSpinnerAdapter( adapter, R.layout.contact_spinner_row_nothing_selected, // R.layout.contact_spinner_nothing_selected_dropdown, // Optional this)); 



8. Sieve called SupportMapFragment


Situation

By chance, it was necessary to deal with cards ... good, it was at one time, but even that was enough to bald a little bit povyryvat hair due to stress.
As always, considering that the SDK is absolutely reckless and works like a clock, I grabbed a memo. At that time, I was delighted with the recently installed LeakCanary , praised him mentally and began to study the logs. They were strange (for example, it was com.google.android.gms.location.internal.zzk ), but they said that my MapFragment was leaking . What did I find in the end, after an hour of studying my source code, up and down? Well, I think the answer is so clear.

In fact..

And in fact, the SDK is guilty and he is with her. I confess, my mistake, it was necessary to immediately pay attention to the strange logs, but somehow did not grow together. Logs in LeakCanary are often not particularly clear, except for the last lines, where you can see exactly “your” links, so everything else was safely ignored. Personally, I ran into the following problems, which, by the way, grabbed at once:
  1. A leak
  2. OutOfMemoryError №1
  3. OutOfMemoryError №2
  4. BadParcelableException

The last bug was especially unpleasant. For the first time using the Parceler library, I decided that the bug in it or in the fact that I was using it incorrectly. The idea that the bug arose because of the SupportMapFragment didn’t arise at all - agree, and here the cards and the BadParcelableException , which occurs when you personally add and pull out some data from the Bundle ? And so I spent several hours again, studying the source code for Parceler and Bundle.putParcelable () as insane.

Conclusion


Despite all the problems and oddities mentioned here, as well as the general tone of the article, I still like programming for android. Yes, sometimes the SDK slaps one another, but in general it provides many other, well-implemented (!) Features. What are the new Toolbar , NavigationDrawer and Behavior ? What to say about the Shared Element Activity Transition !
I wanted to achieve only one article - so that if you have not yet encountered similar problems, when confronted, you would immediately go to Google, rather than sit for an hour eating in debug. I plan to write 2 more parts of the "cuvettes": SDK + libraries and RxJava, but, of course, everything depends on the results of this part.
For beginners, as well as for average programmers, I highly recommend reading CodePath Android Cliffnotes at your leisure. It does not affect the “cuvettes” (although not without it), but it provides a very detailed description of the entire SDK.

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


All Articles