Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Hidden gems and wats for modern Android Development

Hidden gems and wats for modern Android Development

Saul Molinero

December 20, 2019
Tweet

More Decks by Saul Molinero

Other Decks in Programming

Transcript

  1. @_saulmm <resources> <string name=“app_name”> </string> <string name="txt_selection">%1$d selected items</string> <string

    name="txt_feedback">Not happy with ? support: [email protected]</string> </resources> Repunantiño Repunantiño
  2. @_saulmm XML Entities <resources> <string name=“app_name”> </string> <string name="txt_selection">%1$d selected

    items</string> <string name="txt_feedback">Not happy with ? support: </string> </resources> <!DOCTYPE resources [ <!ENTITY app_name "Repunantiño"> <!ENTITY support_mail "[email protected]"> ]> &app_name; &support_mail; &app_name;
  3. @_saulmm <string name="msg_subscribe"> Sorry. You don\'t have notifications yet.\n Tap

    the megaphone icon to subscribe! </string> binding.txtSubscribe.setText( R.string.msg_subscribe )
  4. @_saulmm <string name="msg_subscribe"> Sorry. <annotation foreground="@color/accent"> You don\'t have notifications

    yet</annotation>.\n Tap the megaphone icon to subscribe! </string> Span annotations https://www.youtube.com/watch?v=x-FcOX6ErdI
  5. @_saulmm spans?.forEach { annotation -> if (annotation.key == "foreground") {

    val color = annotation.getResId(this) if (color != 0) { ssb.setSpan( ForegroundColorSpan(ContextCompat.getColor(this, color)), text.getSpanStart(annotation), text.getSpanEnd(annotation), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } } } Span annotations val text = getText(R.string.msg_subscribe) as SpannedString val ssb = SpannableStringBuilder(text) val spans = text.getSpans(0, text.length, Annotation::class.java) https://www.youtube.com/watch?v=x-FcOX6ErdI
  6. @_saulmm Span annotations <string name="msg_subscribe"> Sorry, <annotation foreground="@color/accent"> You don\'t

    have notifications yet</annotation>.\n Tap the ( <annotation src="@drawable/ic_megaphone"> </annotation> ) icon to subscribe! </string> https://www.youtube.com/watch?v=x-FcOX6ErdI
  7. @_saulmm spans?.forEach { annotation -> } // Other annotations... if

    (annotation.key == "src") { val id = annotation.getResId(this) if (id != 0) { ssb.setSpan( ImageSpan(this, id, ImageSpan.ALIGN_BASELINE), text.getSpanStart(annotation), text.getSpanEnd(annotation), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } } Span annotations https://www.youtube.com/watch?v=x-FcOX6ErdI
  8. @_saulmm spans?.forEach { annotation -> } // Other annotations... if

    (annotation.key == "src") { val id = annotation.getResId(this) if (id != 0) { ssb.setSpan( ImageSpan(this, id, ImageSpan.ALIGN_BASELINE), text.getSpanStart(annotation), text.getSpanEnd(annotation), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } } Span annotations https://www.youtube.com/watch?v=x-FcOX6ErdI
  9. @_saulmm Go to source Log.i("IMPORTANT", "look at my java file

    .(Hello.java:6)") Log.i("IMPORTANT", "For kotlin too! (MainActivity.kt:20)") .([filename].java:[line]) ([filename].kt:[line])
  10. @_saulmm Go to source Log.i("IMPORTANT", "look at my java file

    .(Hello.java:6)") Log.i("IMPORTANT", "For kotlin too! (MainActivity.kt:20)") .([filename].java:[line]) ([filename].kt:[line])
  11. @_saulmm Log.i(TAG, "An information log") Log.d(TAG, "A debug log") Log.v(TAG,

    "A verbose log") Log.e(TAG, "An error log") Log.w(TAG, "A Warning log")
  12. @_saulmm Log.i(TAG, "An information log") Log.d(TAG, "A debug log") Log.v(TAG,

    "A verbose log") Log.e(TAG, "An error log") Log.w(TAG, "A Warning log") 2019-12-12 10:55:55.165 I/yea: An information log 2019-12-12 10:55:55.165 D/yea debug log 2019-12-12 10:55:55.165 V/yea: A verbose log 2019-12-12 10:55:55.165 E/yea: An error log 2019-12-12 10:55:55.165 W/yea: A warning log
  13. @_saulmm Log.wtf // What a terrible failure Log.wtf(TAG, “An ultra

    not expected thing happened") https://developer.android.com/reference/android/util/Log.html#wtf(java.lang.String,%20java.lang.String)
  14. @_saulmm Log /** * What a Terrible Failure: Report a

    condition that should never happen. * The error will always be logged at level ASSERT with the call stack. * Depending on system configuration, a report may be added to the * {@link android.os.DropBoxManager} and/or the process may be terminated * immediately with an error dialog. * @param tag Used to identify the source of a log message. * @param msg The message you would like logged. */ public static int wtf(String tag, String msg) { return wtf(LOG_ID_MAIN, tag, msg, null, false, false); } https://developer.android.com/reference/android/util/Log.html#wtf(java.lang.String,%20java.lang.String)
  15. @_saulmm Log /** * What a Terrible Failure: Report a

    condition that should never happen. * The error will always be logged at level ASSERT with the call stack. * Depending on system configuration, a report may be added to the * {@link android.os.DropBoxManager} and/or the process may be terminated * immediately with an error dialog. * @param tag Used to identify the source of a log message. * @param msg The message you would like logged. */ public static int wtf(String tag, String msg) { return wtf(LOG_ID_MAIN, tag, msg, null, false, false); } https://developer.android.com/reference/android/util/Log.html#wtf(java.lang.String,%20java.lang.String)
  16. @_saulmm Log Log.e(TAG, "") != Log.wtf(TAG, "") Might crash your

    app https://developer.android.com/reference/android/util/Log.html#wtf(java.lang.String,%20java.lang.String)
  17. @_saulmm beginDelayedTransition binding.containerTitle.setOnClickListener { binding.txtContent.text = if (expanded) aLongText() else

    "" binding.txtContent.isVisible = expanded expanded = !expanded } TransitionManager.beginDelayedTransition( binding.root as ViewGroup) https://developer.android.com/reference/android/transition/TransitionManager#beginDelayedTransition(android.view.ViewGroup)
  18. @_saulmm beginDelayedTransition() package android.transition; /** * Convenience method to animate,

    using the default transition, * to a new scene defined by all changes within the given scene root between * calling this method and the next rendering frame. */ public static void beginDelayedTransition(final ViewGroup sceneRoot) { beginDelayedTransition(sceneRoot, null); } https://developer.android.com/reference/android/transition/TransitionManager#beginDelayedTransition(android.view.ViewGroup)
  19. @_saulmm getDrawable ContextCompat.getDrawable( @NonNull Context context, @DrawableRes int resId )

    ResourcesCompat.getDrawable( @NonNull Resources res, @DrawableRes int id, @Nullable Theme theme )
  20. @_saulmm getDrawable ContextCompat.getDrawable( @NonNull Context context, @DrawableRes int resId )

    ResourcesCompat.getDrawable( @NonNull Resources res, @DrawableRes int id, @Nullable Theme theme ) AppCompatResources.getDrawable( @NonNull Context context, @DrawableRes int resId )
  21. @_saulmm getDrawable ContextCompat.getDrawable( @NonNull Context context, @DrawableRes int resId )

    ResourcesCompat.getDrawable( @NonNull Resources res, @DrawableRes int id, @Nullable Theme theme ) AppCompatResources.getDrawable( @NonNull Context context, @DrawableRes int resId ) VectorDrawableCompat.create( @NonNull Resources res, @DrawableRes int resId, @Nullable Resources.Theme theme )
  22. @_saulmm ContextCompat.getDrawable( @NonNull Context context, @DrawableRes int resId ) ResourcesCompat.getDrawable(

    @NonNull Resources res, @DrawableRes int id, @Nullable Theme theme ) AppCompatResources.getDrawable( @NonNull Context context, @DrawableRes int resId ) VectorDrawableCompat.create( @NonNull Resources res, @DrawableRes int resId, @Nullable Resources.Theme theme ) getDrawable
  23. @_saulmm getDrawable ContextCompat.getDrawable(…) ResourcesCompat.getDrawable(…) AppCompatResources.getDrawable(…) VectorDrawableCompat.create(…) ContextCompat.getDrawable(…) ResourcesCompat.getDrawable(…) AppCompatResources.getDrawable(…) VectorDrawableCompat.create(…)

    API >= 21 API < 21 android.defaultConfig.vectorDrawables.useSupportLibrary = true. https://stackoverflow.com/questions/43004886/resourcescompat-getdrawable-vs-appcompatresources-getdrawable
  24. @_saulmm @tools:sample <TextView android:id="@+id/txt_content" tools:text="@tools:sample/lorem/random" /> <TextView android:id=“@+id/txt_subtitle" tools:text=“@tools:sample/first_names” />

    <TextView android:id=“@+id/txt_date” tools:text=“@tools:sample/date/hhmm" /> <ImageView android:id=“@+id/img_primary” tools:src=“@tools:sample/avatars” /> <ImageView android:id=“@+id/img_secondary” tools:src=“@tools:sample/avatars” />
  25. @_saulmm @tools:sample app/sampledata/listings.json { "data": [ { "listing": "Brand new

    iPhone" }, { "listing": "Garming 1030 new" }, { "listing": "Toyota Yaris 1.5 hdi 100km" }, { "listing": "Google Home Mini" }, { "listing": "Parrot toy" }, { "listing": "Lawn mower" }, { "listing": "Moving sale" }, { "listing": "Porter cable tools" }, { "listing": "Workout set" } ] }
  26. @_saulmm @tools:sample { "data": [ { "status": “@string/title_status_unconfirmed”, "status_short" :

    “@string/msg_status_unconfirmed" }, { "status": “@string/title_status_confirmed, "status_short" : “@string/msg_status_confirmed” }, { "status": “@string/title_status_delivered”, "status_short" : “@string/msg_status_delivered” }, { "status": “@string/title_status_delivering”, "status_short" : “@string/msg_status_delivering” }, { "status": “@string/title_status_cancelled”, "status_short" : “@string/msg_status_cancelled },
  27. @_saulmm @tools:sample { "data": [ { "status": “@string/title_status_unconfirmed”, "status_short" :

    “@string/msg_status_unconfirmed }, { "status": “@string/title_status_confirmed, "status_short" : “@string/msg_status_confirmed” }, { "status": “@string/title_status_delivered”, "status_short" : “@string/msg_status_delivered” }, { "status": “@string/title_status_delivering”, "status_short" : “@string/msg_status_delivering” }, { "status": “@string/title_status_cancelled”,
  28. @_saulmm Enums in Android “Think of it this way, enums

    are kind of like gremlins, right?” “The price of ENUMS” 3:10 Android Developers - 25 Aug 2015 https://www.youtube.com/watch?v=Hzs6OBcvNQE
  29. @_saulmm Enums in Android Proguard and R8 can optimise simple

    Java and Kotlin enums into ints for you https://www.youtube.com/watch?v=Hzs6OBcvNQE
  30. @_saulmm Enums in Android -optimizations class/unboxing/enum Proguard and R8 can

    optimise simple Java and Kotlin enums into ints for you https://www.guardsquare.com/en/products/proguard/manual/usage/optimizations
  31. @_saulmm Proguard / R8 buildTypes { release { minifyEnabled true

    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } }
  32. @_saulmm Proguard / R8 buildTypes { release { minifyEnabled true

    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } }
  33. @_saulmm Proguard / R8 buildTypes { release { minifyEnabled true

    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } }
  34. @_saulmm onBackPressedDispatcher activity?.onBackPressedDispatcher?.addCallback(this) { } class MyFragment { override fun

    onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) : View? { https://developer.android.com/reference/androidx/activity/OnBackPressedDispatcher
  35. @_saulmm by viewModels val viewModel = ViewModelProviders.of(this, viewModelFactory) .get(DeliveriesViewModel::class.java) //

    Deprecated viewModel = ViewModelProvider(this, viewModelFactory) [DeliveriesViewModel::class.java]
  36. @_saulmm by viewModels val viewModel = ViewModelProviders.of(this, viewModelFactory) .get(DeliveriesViewModel::class.java) viewModel

    = ViewModelProvider(this, viewModelFactory) [DeliveriesViewModel::class.java] // androidx.lifecycle 2.2.0 // Deprecated
  37. @_saulmm by viewModels val viewModel = ViewModelProviders.of(this, viewModelFactory) .get(DeliveriesViewModel::class.java) viewModel

    = ViewModelProvider(this, viewModelFactory) [DeliveriesViewModel::class.java] // androidx.lifecycle 2.2.0 private val viewModel: DeliveriesViewModel by viewModels { viewModelFactory } // Deprecated
  38. @_saulmm by viewModels val viewModel = ViewModelProviders.of(this, viewModelFactory) .get(DeliveriesViewModel::class.java) viewModel

    = ViewModelProvider(this, viewModelFactory) [DeliveriesViewModel::class.java] // androidx.lifecycle 2.2.0 private val viewModel: DeliveriesViewModel by viewModels { viewModelFactory } // activity-ktx 1.1.0 // Deprecated
  39. @_saulmm by viewModels val viewModel = ViewModelProviders.of(this, viewModelFactory) .get(DeliveriesViewModel::class.java) viewModel

    = ViewModelProvider(this, viewModelFactory) [DeliveriesViewModel::class.java] // androidx.lifecycle 2.2.0 private val viewModel: DeliveriesViewModel by viewModels { viewModelFactory } // activity-ktx 1.1.0 // Deprecated
  40. @_saulmm .map private val payment = MutableLiveData<Payment>() val customer =

    payment.map(Payment::customer) // lifecycle-livedata-ktx val customer = Transformations.map(payment, Payment::customer)
  41. @_saulmm map private val payment = MutableLiveData<Payment>() val customer =

    payment.map(Payment::customer) // lifecycle-livedata-ktx val customer = Transformations.map(payment, Payment::customer)
  42. @_saulmm ViewStub “If you have parts of your UI that

    do not need to be visible on first launch, don’t inflate them” Developing for Android, III: The rules: Performance Chet Haase, Android Developers Jun 1, 2015 https://medium.com/google-developers/developing-for-android-iii-2efc140167fd
  43. @_saulmm ViewStub private fun onNetworkError() { binding.stubNetwork.viewStub? .isVisible = true

    } https://medium.com/google-developers/developing-for-android-iii-2efc140167fd
  44. @_saulmm ViewStub private fun onNetworkError() { binding.stubNetwork .viewStub?.isVisible = true

    } private fun onNetworkError() { val view = binding.stubNetwork .viewStub?.inflate() } https://medium.com/google-developers/developing-for-android-iii-2efc140167fd
  45. @_saulmm Selection Tracker class PoiAdapter: RecyclerView.Adapter<PoiViewHolder>() { private val data

    : MutableList<Poi> = mutableListOf() init { setHasStableIds(true) } fun initSelectionTracker(recyclerView: RecyclerView) { } tracker = SelectionTracker.Builder( MainActivity.PO_SELECTION_KEY, recyclerView, StableIdKeyProvider(recyclerView), PoiDetailsLookUp(recyclerView), StorageStrategy.createLongStorage() ).build() lateinit var tracker: SelectionTracker<Long> https://proandroiddev.com/a-guide-to-recyclerview-selection-3ed9f2381504
  46. @_saulmm Selection Tracker class PoiDetailsLookUp(val recyclerView: RecyclerView) : ItemDetailsLookup<Long>() {

    override fun getItemDetails(e: MotionEvent): ItemDetails<Long>? { val view = recyclerView.findChildViewUnder(e.x, e.y) return if (view != null) { (recyclerView.getChildViewHolder(view) as PoiViewHolder) .itemDetails() } else { null } } } https://proandroiddev.com/a-guide-to-recyclerview-selection-3ed9f2381504
  47. @_saulmm Selection Tracker class PoiViewHolder(val binding: ItemRowBinding): RecyclerView.ViewHolder(binding.root) { fun

    bind(item: Poi, isActivated: Boolean) { binding.poi = item binding.root.isActivated = isActivated } fun itemDetails(): ItemDetailsLookup.ItemDetails<Long> = object : ItemDetailsLookup.ItemDetails<Long>() { override fun getSelectionKey(): Long? = itemId override fun getPosition(): Int = adapterPosition } } https://proandroiddev.com/a-guide-to-recyclerview-selection-3ed9f2381504
  48. @_saulmm Selection Tracker class PoiViewHolder(val binding: ItemRowBinding): RecyclerView.ViewHolder(binding.root) { fun

    bind(item: Poi, isActivated: Boolean) { binding.poi = item binding.root.isActivated = isActivated } fun itemDetails(): ItemDetailsLookup.ItemDetails<Long> = object : ItemDetailsLookup.ItemDetails<Long>() { override fun getSelectionKey(): Long? = itemId override fun getPosition(): Int = adapterPosition } } https://proandroiddev.com/a-guide-to-recyclerview-selection-3ed9f2381504
  49. @_saulmm Selection Tracker fun addSelectionObserver(selectionObserver : SelectionTracker.SelectionObserver<Long>) { tracker.addObserver(selectionObserver) }

    poiAdapter.addSelectionObserver(object : SelectionTracker.SelectionObserver<Long>() { override fun onSelectionChanged() { super.onSelectionChanged() val count = poiAdapter.tracker.selection.size() val hasSelection = !poiAdapter.tracker.selection.isEmpty // Start action mode here } }) https://proandroiddev.com/a-guide-to-recyclerview-selection-3ed9f2381504
  50. @_saulmm Build scans // From gradle 4.3+ https://scans.gradle.com/s/c73b5gmuvewi2 BUILD SUCCESSFUL

    in 1m 8s 68 actionable tasks: 68 executed Publishing a build scan requires accepting the Gradle Terms of Service. Do you accept these terms? [yes, no] y Gradle Terms of Service accepted. Publishing build scan... ➜ ./gradlew build --scan
  51. @_saulmm Rename APKs / AABs defaultConfig { // ... setProperty(

    "archivesBaseName", "popsy-$versionName@${rootProject.checkedCommit()}" ) }
  52. @_saulmm Android Build Tools android { compileSdkVersion 29 dataBinding {

    enabled = true } defaultConfig { buildToolsVersion "29.0.2"
  53. @_saulmm Android Build Tools android { compileSdkVersion 29 dataBinding {

    enabled = true } defaultConfig { // This is optional from android plugin 3.+ buildToolsVersion "29.0.2"
  54. @_saulmm AOSP gems /** * Used to determine whether the

    user making this call is subject to * teleportations. * * <p>As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this method can * now automatically identify goats using advanced goat recognition technology.</p> * * @return Returns true if the user making this call is a goat. */ public boolean isUserAGoat() { return mContext.getPackageManager() .isPackageAvailable("com.coffeestainstudios.goatsimulator"); } UserManager.java:1220
  55. @_saulmm AOSP gems ActivityManager.java:3375 /** * Returns "true" if the

    user interface is currently being messed with * by a monkey. */ public static boolean isUserAMonkey() { try { return getService().isUserAMonkey(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } }
  56. @_saulmm AOSP gems ActivityManager.java:2199 /** * Feature for {@link #getSystemAvailableFeatures}

    and * {@link #hasSystemFeature}: The device's touch screen is capable of * tracking a full hand of fingers fully independently -- that is, 5 or * more simultaneous independent pointers. */ @SdkConstant(SdkConstantType.FEATURE) public static final String FEATURE_TOUCHSCREEN_MULTITOUCH_JAZZHAND = "android.hardware.touchscreen.multitouch.jazzhand";
  57. @_saulmm AOSP gems SensorManager.java /** Gravity (estimate) on the first

    Death Star in Empire units (m/s^2) */ public static final float GRAVITY_DEATH_STAR_I = 0.000000353036145f; /** Gravity on the island */ public static final float GRAVITY_THE_ISLAND = 4.815162342f; /** * A constant describing a Tricorder See * {@link android.hardware.SensorListener SensorListener} for more details. * * @deprecated use {@link android.hardware.Sensor Sensor} instead. */ @Deprecated public static final int SENSOR_TRICORDER = 1 << 6;
  58. @_saulmm AOSP gems AndroidSdkVersion.java /** Android 4.3. The revenge of

    the beans */ public static final int JELLY_BEAN_MR2 = 18; /** Android 4.4. KitKat, another tasty threat. */ public static final int KITKAT = 19; /** Android 5.0. A flat one with beautiful shadows. But still tasty. */ public static final int LOLLIPOP = 21;