The post Splash Screens: The Final Right Way appeared first on Big Nerd Ranch.
]]>targetSdk
. In Android 12, there are quite a few changes affecting our users and in this blog post we will focus on the upcoming App Splash Screen changes.
Splash screens have quite a history with Android, and the Ranch. In a previous post here we detailed the “right way” to add a splash screen to your Android app by overriding android:windowBackground
. Now, splash screen support has been grafted into the Android platform (and backported with an androidx library). Starting with Android 12, App Splash Screens are enabled by default, and if you do not take the time to update your application when targeting Android 12, it may result in an undesirable effect.
To illustrate these changes, I’ve updated an example app from my previous post on DiffUtil. I’ve modified my build.gradle
to change my targetSdk
and compileSdk
to 31
and here are the results on my device running the Android 12 beta:
Not bad. By default on Android 12, the Splash Screen API uses the windowBackground
of your theme if it’s a single color and the launcher icon. But what about the majority of our users, on devices below Android 12?
The same build on a pre-Android 12 device:
Well, that was anticlimactic… 😒
Like most things in the Android world, we can access backports of newer APIs via an androidx
dependency. To start, add the following line to your build.gradle
:
implementation "androidx.core:core-splashscreen:1.0.0-alpha02"
Sync your project with gradle files, then we’ll make an adjustment to our applications theme.
Create a new style, with it’s parent set to Theme.SplashScreen
, along with a few other attributes you can customize:
<style name="Theme.Splash" parent="Theme.SplashScreen"> <item name="postSplashScreenTheme">@style/Theme.App</item> <item name="windowSplashScreenBackground">#FF27140F</item> <item name="windowSplashScreenAnimatedIcon">@drawable/app_icon</item> </style>
postSplashScreenTheme
is a reference to your applications “normal” theme that will be set after your splash screen has finished showing.windowSplashScreenBackground
sets the background color of your splash screen.windowSplashScreenAnimatedIcon
references the icon you wish to show in the center of your splash screen. You may have noticed the “Animated” part of windowSplashScreenAnimatedIcon
😉 we’ll cover that a bit later.Next, modify your application theme in your AndroidManifest.xml
:
<application
--------
android:theme="@style/Theme.Splash"
-------
>
Finally, in your launch Activity
, before setContentView()
is called (order matters here) in your onCreate()
method, add the following method call:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
installSplashScreen() // <---- this one!!
setContentView(R.layout.activity_main)
}
That’s it! Let’s build and run our application on a pre-Android 12 device to see what has changed.
👌 nice
The androidx splash screen library also lets us customize the splash screen’s exit animation. Let’s add a quick fade exit animation:
installSplashScreen().setOnExitAnimationListener { splashScreenView ->
val alpha = ObjectAnimator.ofFloat(
splashScreenView.view,
View.ALPHA,
1f,
0f
)
alpha.duration = 500L
alpha.doOnEnd { splashScreenView.remove() }
alpha.start()
}
}
🎉 It’s always nice to bring new features to older versions of Android, as we know OS updates can take some time to propogate through our user base. But, this doesn’t mean we should not take advantage of new APIs available to us.
You may have read Google’s Splash Screen Guide and noticed a few more features available on Android 12. The Splash Screen platform API supports Animated Vector Drawables and even allows us to add a branding image to our splash screen.
Taking a look at the docs:
The app icon (1) should be a vector drawable, and it can be static or animated. Although animations can have an unlimited duration, we recommend that it not exceed 1,000 milliseconds. By default, the launcher icon is used.
Let’s take adavantage of this by animating a “ring” around our Planets application icon. I’ve converted the VectorDrawable
icon to an AnimatedVectorDrawable
and added this animation:
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
----
[my vector icon]
---
</vector>
</aapt:attr>
<target android:name="ring">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="trimPathEnd"
android:startOffset="500"
android:duration="500"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
Let’s take a look…
Neat!
It’s nice to see Android have a standard for App Splash Screens built into the platform. Up until now, it was common practice to implement your own splash screen for your application. If you did implement a splash screen, it’s important to note any unintended side effects when upgrading your targetSdk
to Android 12. Applications using a separate Activity
for their splash screen without a properly configured theme may result in a user seeing 2 splash screens! 😳
Cheers to another Android release, and thanks for reading.
Happy coding!
P.S. 📣 Special thanks to Clint M. and Donovan L. for design help! 🙌
The post Splash Screens: The Final Right Way appeared first on Big Nerd Ranch.
]]>The post Efficient lists with DiffUtil and ListAdapter appeared first on Big Nerd Ranch.
]]>DiffUtil
and ListAdapter
to avoid those pitfalls, and provide a more efficient user experience, while reducing boilerplate code.
On Android, RecyclerView
is the go-to API for efficiently displaying lists of data. Our data is often changing, so out of the box, RecyclerView.Adapter
comes with many low-level granular APIs to update our user interface as the backing data changes, such as:
notifyItemChanged(position: Int) notifyItemRemoved(position: Int) notifyItemRangeChanged(positionStart: Int, itemCount: Int)
In order to explore these APIs, I’ve created a small sample app listing the known planets of our solar system shown in descending order by their distance from the sun. It seems Pluto has snuck its’ way into our list, so let’s remove it with notifyItemRemoved()
:
adapter.items.remove(pluto) adapter.notifyItemRemoved(plutoIndex)
Assuming you have access to the position of the item to be removed, we can make the appropriate method calls to remove the item from the list and provide smooth animations for our user to visualize the change.
These types of interactions are manageable for small changes but don’t scale well. For a more complex example, let’s have some of our planets change position. (Please ignore the real-world implications of this 🙈) Jupiter moves in front of Saturn, Mars and Earth swap, and Venus is flung out beyond Neptune. Resulting in an order like this:
[ Venus, Neptune, Uranus, Jupiter, Saturn, Earth, Mars, Mercury ]
How would we go about showing these changes to the user with the RecyclerView.Adapter
APIs we have available? notifyItemMoved(fromPosition: Int, toPosition: Int)
comes to mind… But that would involve keeping track of each items’ individual movement. These low-level APIs can be tedious to implement for complex use-cases, and are not always kept in mind when architecting your application. This tends to lead developers to use the “catch-all” notifyDataSetChanged()
method when updating their adapters. From the official documentation, we learn this is not ideal for the best user experience:
This event does not specify what about the data set has changed, forcing any observers to assume that all existing items and structure may no longer be valid. LayoutManagers will be forced to fully rebind and relayout all visible views.
And even goes on to say:
If you are writing an adapter it will always be more efficient to use the more specific change events if you can. Rely on
notifyDataSetChanged()
as a last resort.
Here is an example of notifyDataSetChanged()
displaying our new list of rearranged planets:
adapter.items = rearrangedPlanets adapter.notifyDataSetChanged()
Our data is displayed accurately, and the steps to make that change were much easier, but it resulted in a jarring user experience along with inefficient use of RecyclerView
APIs.
Fortunately, there have been many additions that augment the ease-of-use of RecyclerView
and RecyclerView.Adapter
.
One in particular that has gained some much-needed attention since release is DiffUtil. From the official documentation:
DiffUtil is a utility class that calculates the difference between two lists and outputs a list of update operations that converts the first list into the second one.
Seems simple enough. I have my oldList
, and my newList
. What are the minimum amount of steps needed to migrate oldList
=> newList
and display those steps to my user? –A perfect use-case to display the milky-way’s newly arranged solar system! 🪐
Create a new class, extending DiffUtil.Callback
. We supply this class with our new and old list and it calculates a DiffResult.
areItemsTheSame()
determines if oldItem
and newItem
are the same item (usually represented by comparing a unique id). areContentsTheSame()
determines if the values of newItem
and oldItem
have changed. A result is calculated determining the item’s new position (if it was moved), and it’s new contents (if it has changed).
class PlanetDiffUtilCallback(private val oldList: List<Planet>, private val newList: List<Planet>): DiffUtil.Callback() { override fun getOldListSize(): Int { return oldList.size } override fun getNewListSize(): Int { return newList.size } override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition].id == newList[newItemPosition].id } override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition] == newList[newItemPosition] } }
Add a method to our PlanetAdapter
(a subclass of RecyclerView.Adapter
):
fun update(newList: List<Planet>){ val callback = PlanetDiffUtilCallback(items, newList) val result = DiffUtil.calculateDiff(callback) currentItems = newList // don't forget to update the backing list result.dispatchUpdatesTo(this) }
From our Activity
or Fragment
:
planetBasicAdapter.update(rearrangedPlanets)
How wonderful! Each modified planet in our RecyclerView
is updated with its new location, and the user is notified of the granular changes in the list with smooth animations 😎.
As perfect as DiffUtil
seems, it does come with some limitations. From the documentation :
The actual runtime of the algorithm significantly depends on the number of changes in the list and the cost of your comparison methods… Due to implementation constraints, the max size of the list can be 2^26.
So DiffUtil
could run into issues on very large lists with heavy modifications. It’s also worth noting, that in our current implementation, the “diffing” calculation is being done on the main thread. This could lead to unwanted UI “jank”. Fortunately, DiffUtil
offers a way to move its calculation to a background thread.
To send DiffUtil
calculations to the background, Android offers us AsyncListDiffer. For simplicity, the ListAdapter wrapper class can be used instead of AsyncListDiffer
directly. Below is a modified version of our above example, using ListAdapter
:
class PlanetListAdapter: ListAdapter<Planet, PlanetViewHolder>(PlanetDiff) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlanetViewHolder { return PlanetViewHolder(ItemPlanetBinding.inflate(LayoutInflater.from(parent.context), parent, false)) } override fun onBindViewHolder(holder: PlanetViewHolder, position: Int) { holder.bind(getItem(position)) } }
PlanetDiff
object:
object PlanetDiff: DiffUtil.ItemCallback<Planet>(){ override fun areContentsTheSame(oldItem: Planet, newItem: Planet): Boolean { return oldItem == newItem } override fun areItemsTheSame(oldItem: Planet, newItem: Planet): Boolean { return oldItem.id == newItem.id } }
ListAdapter
‘s implementation requires us to override onCreateViewHolder()
and onBindViewHolder()
, 1 less method than a normal RecyclerView.Adapter
. For most use-cases, ListAdapter
should be our go-to RecyclerView.Adapter
class because of its reduced boilerplate and added functionality.
From our Activity
or Fragment
:
planetAdapter.submitList(rearrangedPlanets)
That’s it! Now the application is performing its diff calculation on a background thread, saving our user from any potential lag. We also wrote less code and reduced boilerplate using ListAdapter
.
This simple example shows how ListAdapter
and DiffUtil
can be used to avoid hardship without sacrificing performance while displaying and modifying lists of data. APIs like this have been a great addition to the Android platform, so much so that other platforms have followed suit, implementing their own versions of DiffUtil
, all based on the Myers difference algorithm.
The post Efficient lists with DiffUtil and ListAdapter appeared first on Big Nerd Ranch.
]]>