Share
Embed
Transcript
110 slides
-
@_saulmm android.R.string <TextView android:text="@android:string/no" /> <TextView android:text=“@android:string/yes" /> <TextView android:text=“@android:string/cancel”
/> <TextView android:text=“@android:string/ok” /> -
@_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 -
@_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; -
@_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 ) -
@_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 -
@_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 -
@_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 -
@_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 -
@_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 -
@_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]) -
@_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]) -
@_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") -
@_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 -
@_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) -
@_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) -
@_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) -
@_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) -
@_saulmm @+id/ <EditText android:layout_width="200dp" android:layout_height="wrap_content" /> Without an id, content
clears with a configuration change -
@_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) -
@_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) -
@_saulmm animateLayoutChanges private fun onAddItemClick() { DataBindingUtil.inflate<ItemExpandableBinding>( layoutInflater, R.layout.item_expandable, binding.containerExpandables,
true ) <LinearLayout android:id="@+id/container_faq" android:animateLayoutChanges="false"> <.../> -
@_saulmm animateLayoutChanges private fun onAddItemClick() { DataBindingUtil.inflate<ItemExpandableBinding>( layoutInflater, R.layout.item_expandable, binding.containerExpandables,
true ) <LinearLayout android:id="@+id/container_faq" android:animateLayoutChanges=“true"> <.../> -
@_saulmm getDrawable ContextCompat.getDrawable( @NonNull Context context, @DrawableRes int resId )
ResourcesCompat.getDrawable( @NonNull Resources res, @DrawableRes int id, @Nullable Theme theme ) -
@_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 ) -
@_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 ) -
@_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 -
@_saulmm getDrawable ContextCompat.getDrawable(…) ResourcesCompat.getDrawable(…) AppCompatResources.getDrawable(…) VectorDrawableCompat.create(…) ContextCompat.getDrawable(…) ResourcesCompat.getDrawable(…) AppCompatResources.getDrawable(…) VectorDrawableCompat.create(…)
API >= 21 API < 21 https://stackoverflow.com/questions/43004886/resourcescompat-getdrawable-vs-appcompatresources-getdrawable Caused by: android.content.res.Resources$NotFoundException: -
@_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 -
@_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” /> -
@_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" } ] } -
@_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 }, -
@_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”, -
@_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 -
@_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 -
@_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 -
@_saulmm Proguard / R8 buildTypes { release { minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } -
@_saulmm Proguard / R8 buildTypes { release { minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } -
@_saulmm Proguard / R8 buildTypes { release { minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } -
@_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 -
@_saulmm by viewModels val viewModel = ViewModelProviders.of(this, viewModelFactory) .get(DeliveriesViewModel::class.java) //
Deprecated viewModel = ViewModelProvider(this, viewModelFactory) [DeliveriesViewModel::class.java] -
@_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 -
@_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 -
@_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 -
@_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 -
@_saulmm .map private val payment = MutableLiveData<Payment>() val customer =
Transformations.map(payment, Payment::customer) -
@_saulmm .map private val payment = MutableLiveData<Payment>() val customer =
payment.map(Payment::customer) // lifecycle-livedata-ktx val customer = Transformations.map(payment, Payment::customer) -
@_saulmm map private val payment = MutableLiveData<Payment>() val customer =
payment.map(Payment::customer) // lifecycle-livedata-ktx val customer = Transformations.map(payment, Payment::customer) -
@_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 -
@_saulmm ViewStub private fun onNetworkError() { binding.stubNetwork.viewStub? .isVisible = true
} https://medium.com/google-developers/developing-for-android-iii-2efc140167fd -
@_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 -
@_saulmm Selection Tracker dependencies { // ... implementation 'androidx.recyclerview:recyclerview-selection:1.0.0' }
https://proandroiddev.com/a-guide-to-recyclerview-selection-3ed9f2381504 -
@_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 -
@_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 -
@_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 -
@_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 -
@_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 -
@_saulmm Build scans ➜ ./gradlew aD ➜ ./gradlew iD >
Task :app:installDebug -
@_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 -
@_saulmm Rename APKs / AABs defaultConfig { // ... setProperty(
"archivesBaseName", "popsy-$versionName@${rootProject.checkedCommit()}" ) } -
@_saulmm Android Build Tools android { compileSdkVersion 29 dataBinding {
enabled = true } defaultConfig { buildToolsVersion "29.0.2" -
@_saulmm Android Build Tools android { compileSdkVersion 29 dataBinding {
enabled = true } defaultConfig { // This is optional from android plugin 3.+ buildToolsVersion "29.0.2" -
@_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 -
@_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(); } } -
@_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"; -
@_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; -
@_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;