Tips and advices collected while learning Android
-
- Use an Event wrapper class. LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)
/** * Used as a wrapper for data that is exposed via a LiveData that represents an event. */ open class Event<out T>(private val content: T) { var hasBeenHandled = false private set // Allow external read but not write /** * Returns the content and prevents its use again. */ fun getContentIfNotHandled(): T? { return if (hasBeenHandled) { null } else { hasBeenHandled = true content } } /** * Returns the content, even if it's already been handled. */ fun peekContent(): T = content } class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> { override fun onChanged(event: Event<T>?) { event?.getContentIfNotHandled()?.let { value -> onEventUnhandledContent(value) } } } fun <T> LiveData<Event<T>>.observeEvent(owner: LifecycleOwner, onChanged: (T) -> Unit) { observe(owner, EventObserver(onChanged)) }
-
- When used with
ValueAnimator
for timing animations, it can create really good looking animations in a simple way. TextSwitcher
In XML layout:
<TextSwitcher android:id="@+id/textSwitcher" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:gravity="center" /> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" android:gravity="center" /> </TextSwitcher>
In activity or fragment:
textSwitcher.setInAnimation(this, R.anim.slide_in_from_bottom) textSwitcher.setOutAnimation(this, R.anim.slide_out_to_top) ValueAnimator.ofInt(0, animationDuration).apply { duration = animationDuration.toLong() interpolator = LinearInterpolator() addUpdateListener { if ((it.animatedValue in valueOfInterest) { textSwitcher.setText(texts[keyOrIndex]) } } start() }
- When used with
-
- Change name and extend
AppCompatActivity
instead of Fragment() - Take code from
onCreateView
,onViewCreated
andonActivityCreated
and put in into your Activity'sonCreate
or yourinit()
method and call thatinit()
method fromonCreate
. - Replace the
newInstance
method withgetStartIntent
orstartActivity
as per your use case. Change required variable names for the same. - Replace all the calls to
getActivity
andcontext
withthis
as currently you are in the Activity and it extends the Context class. - If you are using
LiveData
observer, instead of passingviewLifeCycleOwner
, passthis
as Activity is a lifecycle owner. - If using ViewModel, change ViewModel's name to follow activity name.
- Replace all the
childFragmentManager
s withsupportFragmentManager
. - If you are using Dagger for Dependency Injection:
- Change the name of all the modules to match your activity name.
- Change the scope of your activity from
PerFragment
toPerFragment
in your binding module. - In all the fragments which were before added using this Fragment's
childFragmentManager
, when getting ViewModel instance using factory, instead of usingparentFragment
, useactivity
asparentFragment
will return null if the fragment is attached to an activity.
- There can be some more minor changes depending on some function calls, where you need to find their alternatives in activity.
- Change name and extend
-
-
Use
recreate()
inside the activity to reinstantiate the activity. You can do this when you know that a configuration change should take place. For example, let' say you enabled rotation programmatically for your activity at a later time. Then callingrecreate()
will bring the activity in it's proper orientation as per the device orientation. -
Use
configChanges
attribute in your activity manifest entry to avoid configuration change and an activity recreation when any of the things mentioned in the attribute value occur. Handling configuration changes -
Create a
CustomOrientationEventListener
to listen for orientation changes of your device. This is useful when you do not want to recreate your activity on device rotation as that can be too costly in some cases, e.g, when your activity is a camera screen.
abstract class CustomOrientationEventListener(context: Context) : OrientationEventListener(context) { private var previousOrientation = ORIENTATION_UNKNOWN private var currentOrientation = ORIENTATION_UNKNOWN override fun onOrientationChanged(orientation: Int) { currentOrientation = when (orientation) { in 45..134 -> { Surface.ROTATION_270 } in 135..224 -> { Surface.ROTATION_180 } in 225..314 -> { Surface.ROTATION_90 } else -> { Surface.ROTATION_0 } } if (previousOrientation != currentOrientation && orientation != ORIENTATION_UNKNOWN) { previousOrientation = currentOrientation if (currentOrientation != ORIENTATION_UNKNOWN) { onSimpleOrientationChanged(currentOrientation) } } } abstract fun onSimpleOrientationChanged(orientation: Int) }
-
-
- A class that describes a view hierarchy that can be displayed in another process. The hierarchy is inflated from a layout resource file, and this class provides some basic operations for modifying the content of the inflated hierarchy.
- RemoteViews work with only a very limited set of Views and ViewGroups as mentioned here. Link
-
- Answer on StackOverflow
- You can notify your RecyclerView.Adapter's observers to issue a partial update of your RecyclerView.ViewHolders by passing a payload Object.
notifyItemRangeChanged(positionStart, itemCount, payload);
Where payload could be or contain a flag that represents relative or absolute time. To bind the payload to your view holders, override the following onBindViewHolder(viewHolder, position, payloads) method in your adapter, and check the payloads parameter for data.
@Override
public void onBindViewHolder(MyViewHolder viewHolder, int position, List<Object> payloads) {
if (payloads.isEmpty()) {
// Perform a full update
onBindViewHolder(viewHolder, position);
} else {
// Perform a partial update
for (Object payload : payloads) {
if (payload instanceof TimeFormatPayload) {
viewHolder.bindTimePayload((TimeFormatPayload) payload);
}
}
}
}
Within your MyViewHolder.bindTimePayload(payload) method, update your time TextViews to the time format specified in the payload.
-
- When using
FragmentPagerAdapter
, avoid storing data in instance variables inFragment
instance, as they may not have proper state when fragment is removed and added fromViewPager
. To prevent this issue with instance variables, useFragmentStatePagerAdapter
.
- When using
-
- Inject dependencies in
onAttach
befor callingsuper.onAttach
. Reason: https://stackoverflow.com/a/46043545/7891801 - Create your ViewModel in
onViewCreated
. Actually, it can be created in any lifecycle method afteronAttach
, we just need to be consistent about it in our codebase. - If using Kotlin, better to use
by viewModels
delegate function for instantiating ViewModel.
- Inject dependencies in
-
- Use the following extension functions for communicating between fragments and other UI components when using Navigation components, like
startActivityForResult
:
- Use the following extension functions for communicating between fragments and other UI components when using Navigation components, like
fun <T> Fragment.getNavigationResult(key: String = "result") =
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<T>(key)
fun <T> Fragment.setNavigationResult(result: T, key: String = "result") {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, result)
}
-
- Gson tries to serialize property delegates created using
lazy
but fails to do so since there is no backing field for a delegate property by default because it might not store an actual value. This is a general delegate definition, although when we look at theLazy<T>
implementation, we can see that it has a valuefield
but it's unknow to Gson for obvious reasons. More info on delegate properties. This results in a crash as:
Caused by: java.lang.InstantiationException: can't instantiate class kotlin.Lazy
- We can annotate the delegate field with a
@Transient
annotation. But using it directly results in the crash:
This annotation is not applicable to target 'member property without backing field'
- To fix this, we can use this annotation with the annotation use-site target
delegate
which puts the annotation on "the field storing the delegate instance for a delegated property", with the syntax as@delegate:Transient
. As pointed out earlier, sinceLazy
has a field for storing the value, this method works. Example:
@delegate:Transient val area by lazy { calculateArea() }
- Gson tries to serialize property delegates created using
-
- Trying to
break
fromforEach
usingreturn@forEach
does not take the progarm out of theforEach
but on the contrary, acts as a loop'scontinue
and skips the part belowreturn@forEach
for that item and continues with the iteration for the next item. - To get a behavior equivalent to the
break
in a loop, use a "labeled return":
run loop@{ listOf(1, 2, 3, 4, 5).forEach { if (it == 3) return@loop // non-local return from the lambda passed to run print(it) } } print("done with nested loop")
- This will return from the
forEach
after printing till 2. - Explanation at Kotlin's official documentation
- Trying to
-
- The Lombok IntelliJ Plugin gives an error when trying to run on Android Studio versions >= Bumblebee because of version incompatibility.
- To fix it, download the correct plugin version corresponding to the Android Studio version from here and place it in the plugins folder of Android Studio.