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

Hidden gems and wats for modern Android Develop...

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on. →

Hidden gems and wats for modern Android Development

Avatar for Saul Molinero

Saul Molinero

December 20, 2019

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;