Compound view android kotlin

A Kotlin-based Introduction to Compound Components on Android — Part 1

Creating awesome custom views on Android by leveraging the power of Compound Components.

Series Roadmap

Android provides us with a huge set of views that already cater to our basic needs. Whether it’s a TextView to display the status of a launched transaction, or an ImageView to showcase a retrieved image from a URL, all of these views are amazing, and we can always change few attributes handed to us by Android to customize the already existing views to our needs.

But that is not always the case.

At the same time, designing an application that’s unique across android’s “design” scheme, and does not necessarily depend on existing android views to mock its look and feel is a skill that is very great to have up our sleeves. In this light, we are presented with Custom Views.

Custom Views are views that subclass existing View or ViewGroup implementations to provide absolute control over the appearance and functions of the element.

Custom Views in android can be created in two ways:

  1. Extending a View: This process entails creating a subclass of the View class or any of its subclasses, giving us implementation data present in a basic android view to work with, after which overriding can then follow.
  2. Extending a ViewGroup: Views that accommodate other views as children are referred to as ViewGroups — Layouts. Examples are LinearLayout, ConstraintLayout, RelativeLayout and a lot more. These ViewGroups can also be extended by grouping various views with different functions into a single ViewGroup, and then referencing the ViewGroup and its sub-elements with a single view name — Compound Components.

Sounds a bit obscure? don’t worry, hang in there.

This article discusses Compound Components, and how they can be used in isolating a certain number of elements, whose functionality is closely related, and would be needed more than one time across the whole application. Most times, you might not need to extend a basic view and provide custom implementations to it, the design you’re looking for might be attained by joining a set of views already provided by the Android Framework, and Voila!, your compound component is ready!

Now that you understand what Compound Components are, let us jump right in!

P.S: If the whole thing still feels blurry, you can always check out what the smart folks at Google had to say about the whole topic here. Cheers!

What would we be building?

At the end of this three-part tutorial on compound components, we should come up with this application right here! 👇🏾

The view to be built is what I refer to as a File Descriptor (not so fancy?, I know). It is a compound component that provides the functionality of displaying information about selected files present on the Android File System, and these include a preview of the file itself, its name, the date it was last modified, size of the file, and a bit more!

Excited? let’s dive right in!

To be able to follow this article closely, I expect you to at least be familiar with Android development and the Android SDK. Also, some Kotlin-specific features would be used in this tutorial, but that should not be a problem.

The starter code for this project can be found here. It basically is a new android project with an empty activity, it also contains a set of vector drawables and images that would be used across the entire project.

Whew!, this is going to be a long ride! you might want to grab a cup of coffee☕

Creating Our Compound Component

To create our compound component, we would need to put two important pieces together — layout and logic. The layout contains the views we would be joining together to create our compound component, while the logic would contain specific functions the views in the compound component could execute.

Читайте также:  Android alertdialog change button color

The Layout

Remember, a compound component has to extend an existing ViewGroup implementation, so in this article, we would be extending the ConstraintLayout. ConstraintLayout is an amazing ViewGroup provided to us by Android that lays out its items by attaching constraints on one View to another in the ViewGroup or the parent itself. This helps hugely in eliminating nested view hierarchies.

To create the layout, navigate to res > layout , and create a new file labelled file_descriptor.xml , below is the code for the layout.

In the code above, there are five main views, the first ImageView whose purpose is to show a preview of the file (image/video) selected, the small image with id: file_type_image aligned to the mid-left of the component denotes the type of file selected. There are also two text views that display the name and information about the file selected respectively. Then, we have two icons whose functions are to share and toggle the information state.

Before we move any further, if you downloaded the starter code for this article, you’d have in the drawables folder, a set of icons that would be used in the application, so you have nothing to worry about!.

Our layout is almost ready!, but there is a teeny-tiny issue, remember when I said a compound component is actually a ViewGroup accommodating views with closely-related functions, yes! exactly!. The idea here is to create an independent layout and then connect the views to an implementation of a certain ViewGroup — ConstraintLayout. But the issue here is, the layout created above has a ConstraintLayout as its parent, so doesn’t that mean that we are done? not quite😤!.

That is not the behaviour we want, if we go on with that layout, we would end up adding the parent of that layout — ConstraintLayout as a child of our Compound Component which happens to be a ConstraintLayout also, so our Compound Component would end up having one and only one direct child which would be a ViewGroup. This does not quite give us direct access to our inner views, so how do we go about this?

The Merge Tag

Android provides us with the merge tag. The merge tag takes a group of views and merges them together. So, instead of ordering them under a ViewGroup, they are independent and without a parent. Following this method, our views would be added directly as children of our Compound Component.

So, our file_descriptor . xml file can be modified as such:

At this point, your layout is probably looking disorganized, don’t worry, we’d be done in a sec.

The Logic

Now that the layout is all set, what’s left is to connect it to a ViewGroup implementation. Go ahead and create a file called FileDescriptor.kt , this class would contain our logic for implementing our compound component.

As a reminder, we would be extending the existing implementation of the ConstraintLayout class, so now would be a good time to see how the class looks like.

Let’s start by comprehending the anatomy of our ConstraintLayout class, it has three constructors.

  • ConstraintLayout(Context context): this constructor takes in one parameter — the context of the activity inflating the layout
  • ConstraintLayout(Context context, AttributeSet attrs): this constructor accepts two parameters — the context (see above), and the set of attributes defined for this layout
  • ConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr): this constructor takes in one additional parameter defStyleAttr, this denotes the default style to be used for this layout, usually defaults to 0.

Extending the ConstraintLayout class demands we provide adequate calls to the constructors of the superclass, so we don’t get caught “off-guard”. So, we have to provide constructors in our Compound Component that effectively invokes all three of the constructors present in the superclass. So, let us start by doing that! Below is the code for our FileDescriptor class.

In the code above, we declare explicitly, three constructors matching the ones present in the superclass. Amazing!.

But if you are a fan of Kotlin (like me), this approach isn’t really the best. Kotlin provides optional and default function parameters that allow us to specify parameters that might be included or omitted in the function invocation and default values for those that weren’t included. One more thing is: the class being extended is written in Java, and we are about to use some Kotlin-specific features in here, so there would be issues as Java does not know about default or optional parameters.

So, here comes the @JvmOverloads annotation. This annotation carefully converts the default and optional parameters used in the function declaration to independent functions with separate parameters when transpiled to Java. Here is how the class looks after using default and optional parameters.

Читайте также:  Настройка airpods для андроида

By adding the @JvmOverloads annotation, we must provide a secondary constructor that takes in all the arguments supposed to be passed into the ConstraintLayout constructors. As you can see above, only the Context object ctx is compulsory, since all constructors of the ConstraintLayout class accept it. The second parameter attributeSet is optional since only two of the constructors accept it, and it defaults to null if none is passed in originally. The final parameter defStyleAttr also defaults to 0 if none is provided. The annotation takes care of creating the constructors in the Java equivalent of this class.

At this point, we are one step away from showing our layout on the screen. We are going to take advantage of the init block given to us by Kotlin, it serves as a block of code that runs whenever any of the constructors is used to create an object of the class.

So, what exactly do we want to do in the init block?

Inflate the layout

Yeah!, you heard that right. Inflating layouts is a very common concept in android development, as it processes the tags created in our XML layouts and converts them to their equivalent objects in code — Inflation . This concept is widely used when creating fragments or instantiating viewholders for a recyclerview.

This process entails getting the views declared in the XML for this component and then inflating them as children of this compound component.

To inflate the layout for the compound component into this class, we go through two steps.

  1. Get the layout inflater service — the object that does the inflating
  2. Inflate!

The first step is to get the inflater service from the android system by invoking getSystemService() on the context object and casting it to a type of LayoutInflater. We then inflate the layout into our compound component and we’re done!

Finally, we set the background of our component by calling setBackground and passing in our drawable resource. And that’s actually it!.

Showing on-screen

After all the setup, we can now call our custom view using its fully-qualified name in our XML layout by inserting the code below.

Android studio might not know how to render the view straightaway in your layout preview, so you might want to rebuild the project, so it records the changes made to the component.

And you should come up with this!

What’s next?

We are not done with the application yet, though we have learnt a lot, we still have to wire up the functions of the view, and we would cover that in the next part right here!

Источник

Exploring Kotlin initialization with Android custom views

A closer look at the relationship between Kotlin and programmatic/dynamic view inflation

Today, we explore the question “Where does initialization occur around the lifecycle of a view?” This is topic my teammates hotly debated over for months, so I’ve decided to see for myself. This blurb covers two ways of inflating a custom view for comparison: via layout resource and programmatically.

For this exploration, we look at a small compass application. For convenience, you can download the code on Github to follow along/run the app on your own device.

This app uses View Binding and a custom view to spin the needle based on where the user’s device is facing. The two different cases exist on different branches from the main branch.

When are Kotlin constructors and init blocks called?

Like Java, Kotlin can declare multiple constructors, but makes a differentiation between primary and secondary constructors. Both are denoted with the keyword constructor .

In most cases, a Kotlin class will only use a primary constructor. If there are no visibility modifiers or annotations, then a class can omit the constructor keyword. A Kotlin class without a primary constructor exists will generate one that does nothing.

A primary constructor may also include an initializer block denoted with the keyword init . The initializer block executes the logic as soon as the class is initialized as part of the primary constructor.

Secondary constructors are mostly used for Java interoperability. For the case of CompassView below, no primary constructor is declared, but multiple secondary constructors are:

These secondary constructors will delegate to the proper constructor in the super class, or find one that does. But in what order, specifically, would a secondary constructor execute in relation to the primary constructor?

From Kotlin official documentation:

Delegation to the primary constructor happens as the first statement of a secondary constructor, so the code in all initializer blocks and property initializers is executed before the secondary constructor body.

In CompassView , the initialization block executes before the secondary constructors do. But where does Kotlin class initialization fall in relation to the View lifecycle, exactly? We answer this by examining how a view is inflated.

Читайте также:  Spotify 4 pda android

What happens when a view is created?

The answer depends on how a view is added in a tree. On the screen, all views in Android exists in a single tree. You can add views to that tree two ways:

  • Programmatically: adding a View to that tree.
  • XML: specifying another tree to via an Android layout file.

In our compass application, we inflate our custom CompassView on to the MainActivity as the only custom view component. This article demonstrates the differences between inflating a custom view both via XML ( CompassView(context, attrs) ) and code( CompassView(context) ).

Case 1: Custom Kotlin View inflated via XML

When you put a view element into an XML file, Android will use a LayoutInflater to parse and map the corresponding objects in the XML to the inflated Views. LayoutInflater retrieves the resource by opening up ResourceManager and examining all the possible layouts it could match within its current configuration. Android will then parse back the appropriate binary XML resource.

For CompassView(context, attrs) the second argument will utilize the properties necessary to tell the view hierarchy how to size and place the element in the tree. This post does not focus on these phases (Measure/Layout/Draw) of the View lifecycle, but there is a totally awesome talk from Drawn out: How Android renders (Google I/O ’18) that dives deep down into the mechanisms for whomever is curious.

This process of inflation is then repeated for its children and its children’s children, recursively until all views have been inflated. When inflation is complete, LayoutInflater sends a callback onFinishInflate() from the children views back up to the root to indicate the view is ready to interact with.

Now that we’ve described the process a bit more, let’s examine the code needed to instantiate our custom CompassView with XML:

In MainActivity , the CompassView via the view binding reference from the XML. When initializing the custom view this way, the default secondary constructor is executed:

The Kotlin initialization block is called first, then the secondary constructor CompassView(context, attrs) . Remember, there is no primary constructor for extending a View class (unless you create one yourself), so it makes sense that init is executed first before the secondary constructor does. Because calling CompassView(context, attrs) uses LayoutInflater , the onFinishInflate() callback is made when all views have finished inflating.

Case 2: Custom Kotlin View inflated programmatically

For the most part, initializing views via XML is the preferred way of creating view elements in Android since it becomes part of the tree view hierarchy. There are advantages to this: using XML is much more friendly for Android memory since it can compress easily, and helps to take off runtime workload a programmatic draw might have to do.

Suppose you have a case where you cannot include the creation of a custom view in the XML, but rather, you must initialize the view only at runtime.

This is what the code might look like:

As you can see, there’s a significant work load setting up the UI programmatically just to make the CompassView element fit in with ConstraintLayout. This particular case would be incredibly impractical in real life, but there is a special reason we talk about initializing CompassView(this) . To demonstrate, we’ll run this code:

Like the previous case, the Kotlin initialization block executes first, then the secondary constructor CompassView(context) . However, you might have noticed that no onFinishInflate() callback is ever made. This is because CompassView(context) is not instantiated by XML, meaning that no LayoutInflater is put into play. There’s no children to wait on for recursive instantiation, and so there’s no onFinishInflate() for callback.

If you needed to add shared logic for different constructor calls that doesn’t depend on this system call, this might pose a problem for creating more manual view bindings in older code and other UI-related actions.

The Recommendation

You can annotate a custom view with the @JvmOverloads annotation, which tells the Kotlin compiler to generate overloaded methods. Every overload will substitute with default values for any parameters omitted in the generated methods, which means that the code below is equivalent to the CompassView class constructors written in the beginning of this article:

Instead of depending on the system to call onFinishInflate , you can use Kotlin init < >for shared logic (even with views dynamically inflated in the code). Running the inflate method in the initialization block will guarantee inflation occurs no matter how you choose to initialize your custom view.

I hope you enjoyed this blurb: in Android, looking at certain concepts through the lens of Kotlin can shed new perspective to seemingly basic questions. You can find the recommended version of the code on the main project, and additional resources below.

Источник

Оцените статью