Categories
How To Studio

Top Kotlin utils we use all over our project

Michael Spitsin

Over the years, I’ve been faced with posts and articles dedicated to useful extensions and utilities here and there. “Top 5 Convenient Kotlin Extensions”, “Top 10 Super Kotlin Utilities”, “Top 20 Neat Extensions”, “Top 100 Most Needed”. You can find many articles on the internet: Here, There, this, It, Here And even here..

I don’t want to look into each and comment on what’s offered. Instead, I like the efforts that library authors are making to make our lives easier. And that is our call and what kind of experience we will adopt. I also got the idea of ​​sharing extensions and utility methods to help with project development.

Prepare tea / coffee to share different areas of improving code, not just 5 or 20 ways.

If you like this article, please applaud. In one of the following articles, we’ll have another pack of extensions / utilities.

Android has a way to manipulate and retrieve resources. res folder.You use context Or resources Takes an integer, a string, etc. But imagine the following case:

  • You are using ViewHolder At the binding stage, you need to get the dimension as an integer.Then you need to write something like
viewHolder.itemView.context.resources.getDimensionPixelSize(R.dimen.my_dimen)
//which is quite long :)
  • There is presentation logic that splits from the Android UI side and wraps the resource so that it doesn’t use the Android context / resource directly. For example, there is a mapper that builds UI representations based on a domain model. And you want to cover it with unit tests.

In both cases:

  1. Of course, it will be ignored. Not all projects and teams have unit test policies. And when you’re constantly looking at long, repetitive codes, you don’t necessarily feel pain in your mind.
  2. The first problem can be solved by creating BaseViewHolder And the basic way to get the int dimension. The second problem can be solved by mocking the context / resource call

It solved both issues while providing a way to work with the old “interface-implementation” structure for unit testing without having to mock Android-specific ones.

Please note that we made AppResourcesProvider As an inline class. This means that you can avoid redundant object allocations in some situations. But, of course, that’s not the case with mappers. You need to provide the actual implementation to pass a reference to the interface.

In the case of the above scenario ViewHolder Consider looking up the old one in front of me Articles on building declarative adapters..There is a section called Resource administratort. Simply put, we have the following base view holders:

Much simpler than declaring all the methods in it ViewHolder manually.

This approach has several important advantages, in addition to being split into individual entities, more concise usage, and unit testing.

  • As mentioned in some places, it does not create an instance of that resource manager. This can be useful for other extensions or approaches in your code. Consider the following example (which is also used in our project).you have view-binding It’s a library and I want to have easy access to resources (I hope more articles on view bindings and view adapters will be provided later).Just provide a single extension
val ViewBinding.res get() = AppResourcesProvider(root.context)//now compare
//1. root.context.resources.getColor(R.color.white)
//2. res.getColor(R.color.white)
//And no object allocation, since you using `AppResourcesProvider` directly
//Bonus, you can also write that extension for View
val View.res get() = AppResourcesProvider(context)

In our project.Well, in every project I worked on, sometimes there were scenarios you could say id Or code Or something, and you want to find an enum by it.

Previously, I didn’t even have something like

And I had to do that for all enums, with a glimpse of the filtering requirements. Not so anymore!

In general, there are two methods.

  1. requireEnumBy — Returns an enum of types T this is predicate Passed or used fallback In that case, a callback to return the default enum
  2. findEnumBy — Returns an enum of types T Null if no enumeration matches the criteria.

Let’s see how these methods can be used in the above example

The only drawback of this approach is val code: String Or other enum fields participating in filtering public.. The vanilla approach can be private 🤔

You can quickly create a workaround.

But in that case enum Utility methods are not that big.In our project using public val For enums, it doesn’t matter. Most of them are considered models and it’s okay to expose the model fields. But I just wanted to warn you 🙂

You may want to change the offset of the view. Increase padding and reduce parsing. And let’s say you need to adapt dynamically for some reason.

When we’re talking, we usually pad everything more or less easily get Method.Please use one view.paddingStart, view.paddingTop Such. However, to place a new padding, you need to do the following:

view.setPaddingRelative(
newPadding,
view.paddingTop,
view.paddingEnd,
view.paddingBottom
)

This is very inconvenient. I don’t make KTX Your situation is much better:

view.updatePaddingRelative(start = newPadding)

I already wrote a few years ago about another weird KTX solution using bundles and how to make it more type-oriented and not lose key key features. This makes the bundle more than just a map. If you are interested, please jump in.

Margins make things worse because you can’t get or set margins in the view because this information is relevant to. MarginLayoutParams.. Again, KTX offers a pretty weird solution. On the one hand, they say, “There are a lot of useful get methods here.” view.marginStart, view.marginEnd Such.On the other hand, there is no such thing view.marginStart = .. Instead, you should write:

view.updateLayoutParams {
updateMarginsRelative(start = newMargin)
}

This is also not useful for end users (engineers who just want to update poor margins).

The solution is basically to achieve the largest declarative approach possible. Here are some:

view.margin.start = newMargin
view.padding.total = newPadding
view2.padding.bottom = view1.padding.top

And Kotlin inline classesThis allows you to avoid object assignments and make your code more structured in the same way.

And now you can achieve a truly declarative approach that is much more convenient than the KTX solution:

//Just compareview.padding.top = 9
//vs
view.updatePaddingRelative(top = 9)
view.margin.end = 6
//vs
view.updateLayoutParams {
updateMarginsRelative(end = 6)
}

Not only is our solution more readable and declarative. The only advantage is to use: view.padding.vertical = Or view.margin.horizontal = , XML, in some cases can be simplified.

The next version of this solution is to provide an additional inline class responsible for configuring resources.

Suppose you want to set a specific dimension defined in. res folder:

view.margin.start = view.context.resources.getDimensionPixelSize(R.dimen.small)

Or with us AppResourcesProvider From the beginning of the article, you can write:

view.margin.start = view.res.getDimenInt(R.dimen.small)

Now let’s offer another few extensions to make the approach even more convenient.

And now you can just write:

view.marginRes.start = R.dimen.small

It’s much shorter and doesn’t lose readability at the same time.

Sometimes we all have a custom view. You may also add custom attributes (or reuse some Android attributes instead). However, working with them causes headaches over and over again.Don’t forget to use to get all the attributes obtainStyledAttributes Recycle what you receive TypedArray..

Well, actually. Not too bad, but it may be useful to get the idea out of the programmer’s mind. And first, we wanted to say that we would create a special method for that.But then I found it The KTX people have already provided it to us.. Well, they aren’t perfect for our usage, but they provided an easy and good implementation.

Another issue related to getting typefaces from attributes.There is a way getFont This is only available from the 26 API version: (Therefore, this also needs to be handled in some way.

To solve this, we introduced a series of methods.

Let’s talk first use Method. The KTX people provided their own, but they didn’t apply contract in addition. Therefore, you cannot write something like this in their way:

val str: String
context.obtainStyledAttributes(attrSet, R.styleable.SomeView).use {
str = getString(R.styleable.some_text) ?: ""
}

I get the following error:

Initialization of the captured value is due to the possibility of reassignment

Second, I will introduce it here parseAttrs It’s a method, so you don’t have to write it use every time. It turned out to be a small but useful improvement.

And third is the possibility of getting the font AttributeSet..

You can also mock up quick enhancements that give you the possibility to analyze the appearance of your text. AttributeSet This is very useful if you have a custom view that displays the text. Canvas And Paint..

And let’s say you can provide this within your custom view.

Huh! 99% of articles are finished. The last section is a small section, but I decided to include it because it contains only a few extensions and is very important to development.

We are using Resut<T> There are many in our code base.I use coroutines and flows, but I don’t like the idea of ​​writing try/catch Always block.For peers like us, the Kotlin team has created something spectacular runCatching {} Inline method that returns Result<T>.. This inline class contains a number of useful methods for controlling the flow after receiving either the result or the exception + the result (or error).

However, some situations are missing.

  1. If you have a series of calls and the second call depends on the result of the first call, but you can throw an exception, then there is a good way to do it. mapCatching..But what if the second method is already back? Result<T2> Instead of T2?? mapCatching In that case it will return Result<Result<T2>>
  2. If the method throws an exception, you can handle it onFailure Method. However, suppose you need to convert an exception to another exception.In our case, for example, we translate all Retrofit exceptions into more structured ones. OurCompanyException With additional information like backendErrorCode Such. How can you handle it?

Here are two small but powerful ways to solve our problem 🙂

Now suppose we have:

interface Api {
suspend fun getUser(id: Int): Result<User>
suspend fun getAllUsers(name: String): Result<List<User>>
}
class MyCompanyException(
//with a lot of cool stuff inside
) : Exception(...)

Using the above two methods, you can easily create something like this:

fun findAllSimilarUsers(userId: Int): Result<List<User>> = 
api.getUser(id)
.then { api.getAllUsers(it) }
.mapError { MyCompanyException(it) }
//or something like that

If you reach this last point in the article, you’ll be glad to see your applause and likes. If you would like to see more of the utilities and extensions we are using, please leave a comment or applaud;)

Easy sugar coding for everyone!

There are also other interesting articles.

Source

Leave a Reply

Your email address will not be published. Required fields are marked *