Kotlin provides a concise way to reference a member of a class or an instance of a class without invoking it, called member reference. Member reference is a functional feature in Kotlin that allows you to pass a function reference as a parameter to another function, without actually invoking the function. It’s similar to method reference but works with properties and functions that are members of a class or an instance of a class.
In this article, we’ll explore how to use member reference in Kotlin, including syntax, examples, and use cases.
What is a Member Reference?
A member reference in Kotlin is a way to reference a member of a class or interface, such as a property or a method, without invoking it immediately. It is similar to a lambda expression, but instead of providing a block of code to execute, it provides a reference to a member of a class. Member references are useful when you want to pass a function or a property as an argument to another function or class constructor.
Kotlin provides two types of member references: property references and function references. Property references refer to properties of a class, while function references refer to methods of a class.
Property References
Property references allow you to reference a property of a class without invoking it immediately. You can create a property reference by prefixing the property name with the double colons (::) operator. For example, consider the following class:
Kotlin
classPerson(val name: String, val age: Int)
To create a property reference for the name property, you can use the following syntax:
Kotlin
val getName = Person::name
In this example, the getName variable is a property reference to the name property of the Person class. You can use this property reference in many contexts, such as passing it as an argument to a function:
In this example, the printName function takes a property reference to the name property of the Person class and a Person object. It then uses the property reference to print the name of the person.
Function References
Function references allow you to reference a method of a class without invoking it immediately. You can create a function reference by prefixing the method name with the double colons (::) operator. For example, consider the following class:
Kotlin
classCalculator {funadd(a: Int, b: Int): Int {return a + b }}
To create a function reference for the add method, you can use the following syntax:
Kotlin
val calculator = Calculator()val add = calculator::add
In this example, the add variable is a function reference to the add method of the Calculator class. You can use this function reference in many contexts, such as passing it as an argument to a function:
In this example, the performOperation function takes a function reference to the add method of the Calculator class and two integer values. It then uses the function reference to perform the addition operation and print the result.
Member References with Bound Receivers
In some cases, you may want to use a member reference with a bound receiver. A bound receiver is an instance of a class that is associated with the member reference. To create a member reference with a bound receiver, you can use the following syntax:
Kotlin
val calculator = Calculator()val add = calculator::add
In this example, the add variable is a function reference to the add method of the Calculator class, with a bound receiver of the calculator instance.
You can use a member reference with a bound receiver in many contexts, such as passing it as an argument to a function:
In this example, the performOperation function takes a function reference to the add method of the Calculator class, with a bound receiver of the Calculator class. It also takes a Calculator instance and two integer values. It then uses the function reference with the calculator instance to perform the addition operation and print the result.
Use Cases for Member Reference
Member reference can be used in many different contexts, such as passing functions as parameters to higher-order functions or creating a reference to a member function or property for later use. Here are some examples of how to use member reference in Kotlin.
1. Passing a member function as a parameter
One of the most common use cases for member reference is passing a member function as a parameter to a higher-order function. Higher-order functions are functions that take other functions as parameters or return functions as results. By passing a member function as a parameter, you can reuse the same functionality across different contexts.
Kotlin
classMyClass {funmyFunction(param: Int) {// function implementation }}funhigherOrderFunction(func: (Int) -> Unit) {// do something}funmain() {val myClassInstance = MyClass()higherOrderFunction(myClassInstance::myFunction)}
In this example, we have a class called MyClass with a member function called myFunction. We then create an instance of MyClass and store it in the myClassInstance variable. Finally, we pass a reference to the myFunction function using member reference to the higherOrderFunction, which takes a function with a single Int parameter and returns nothing.
2. Creating a reference to a member function for later use
Another use case for member reference is creating a reference to a member function that can be invoked later. This can be useful if you want to invoke a member function on an instance of a class without actually calling the function immediately.
Kotlin
classMyClass {funmyFunction(param: Int) {// function implementation }}funmain() {val myClassInstance = MyClass()val functionReference = myClassInstance::myFunction// ... functionReference.invoke(42) // invoke the function later}
In this example, we have a class called MyClass with a member function called myFunction. We then create an instance of MyClass and store it in the myClassInstance variable. Finally, we create a reference to the myFunction function using the double colon operator and store it in the functionReference variable. We can then use the invoke function to call the myFunction function on the myClassInstance object later.
3. Creating a reference to a member property for later use
In addition to member functions, you can also create a reference to a member property for later use. This can be useful if you want to access a member property on an instance of a class without actually accessing the property immediately.
Kotlin
classMyClass {var myProperty: String = ""}funmain() {val myClassInstance = MyClass()val propertyReference = myClassInstance::myProperty// ... propertyReference.set("softAai") // set the property latervalvalue = propertyReference.get() // get the property later}
In this example, we have a class called MyClass with a member property called myProperty. We then create an instance of MyClass and store it in the myClassInstance variable. Finally, we create a reference to the myProperty property using the double colon operator and store it in the propertyReference variable. We can then use the set and get functions to access the myProperty property on the myClassInstance object later.
4. Bound member reference
Bound member reference is a syntax for referencing a member function of a specific instance of a class. This is useful when you have a function that expects a specific instance of a class as a parameter.
Kotlin
classMyClass {funmyFunction(param: Int) {// function implementation }}funmain() {val myClassInstance = MyClass()val boundReference = myClassInstance::myFunction// ...boundReference(42) // call the function on myClassInstance}
In this example, we have a class called MyClass with a member function called myFunction. We then create an instance of MyClass and store it in the myClassInstance variable. Finally, we create a bound reference to the myFunction function using the double colon operator and store it in the boundReference variable. We can then call the myFunction function on the myClassInstance object later by simply invoking the boundReference function and passing in the necessary parameters.
Member References and Lambdas
In Kotlin, lambda expressions and member references can be used interchangeably in certain situations. This is because a lambda expression that takes an object and calls one of its methods can be replaced with a reference to that method using the double colon operator. This makes the code more concise and readable.
In this code, we have a list of strings and we want to create a new list containing the lengths of each string in the original list. We achieve this using the map function and a lambda expression that takes a string and returns its length.
Now, consider the following code, which achieves the same thing but uses a member reference instead of a lambda expression:
Kotlin
val myList = listOf("abc", "opq", "xyz")val lengthList = myList.map(String::length)
In this code, we use a member reference to reference the length function of the String class instead of the lambda expression. This is possible because the lambda expression only calls a single method on the object it receives, which is exactly what the member reference does.
Let’s take one more example, let’s say you have a class called Person with a method getName() that returns the person\’s name. You can define a lambda expression that calls this method as follows:
Kotlin
val p = Person("amol")val getNameLambda: (Person) -> String = { person -> person.getName() }
Alternatively, you can define a callable reference (method reference) to the same method as follows:
Kotlin
val getNameRef: (Person) -> String = Person::getName
In this case, getNameRef is a callable reference to the getName() method of the Person class, and it has the same type as getNameLambda. You can use either getNameLambda or getNameRef interchangeably in contexts where a function or method of type (Person) -> String is expected.
This interchangeability between lambdas and member references can be useful in situations where you have a lambda expression that only calls a single method on an object, as it can make the code more concise and readable. However, it’s important to note that this interchangeability only works in these specific situations and there may be cases where a lambda expression or a member reference is more appropriate.
Conclusion
Kotlin member references provide a concise and readable way to reference a class’s properties or methods, without invoking them immediately. They are useful when you want to pass a function or a property as an argument to another function or class constructor. Kotlin provides two types of member references: property references and function references. Property references refer to properties of a class, while function references refer to methods of a class. You can also create member references with bound receivers, which allows you to associate a member reference with a specific instance of a class. With member references, Kotlin makes it easy to write concise and readable code that is easy to maintain and understand.
Kotlin is a modern, statically typed programming language that runs on the Java Virtual Machine (JVM). One of its key features is support for lambda expressions, which provide a concise and expressive way to define functions inline. In Kotlin, lambdas can capture local variables, which allows them to extend the scope of those variables beyond the function in which they are declared. This feature is extremely powerful, but it can also be somewhat confusing if you’re not familiar with how variable capturing works. In this article, we’ll explore the topic of variable capturing in Kotlin lambdas in-depth, with plenty of examples along the way.
What is Variable Capturing?
In Kotlin, the lifetime of a local variable is determined by the function in which it is declared. This means that the variable can only be accessed within that function and will be destroyed once the function finishes executing.
However, if a local variable is captured by a lambda expression, the variable’s scope can be extended beyond the function in which it was declared. This means that the code that uses the variable can be stored and executed later.
If the variable is declared as final, its value is stored together with the lambda code that uses it. This is because the value of a final variable cannot be changed once it has been assigned.
On the other hand, if the variable is not final, its value is enclosed in a special wrapper that allows you to change it. The reference to this wrapper is then stored together with the lambda, so that the lambda can access and modify the value of the variable even after the function in which it was declared has finished executing.
This behavior is called “capturing” a variable, and it is a powerful feature of Kotlin’s lambda expressions that allows for more flexible and expressive programming.
Examples
Let’s dive into some code examples to better understand how local variables are captured by lambdas in Kotlin.
First, let’s define a simple function that takes an integer argument and returns a lambda that multiplies its input by a factor:
Kotlin
funmultiplyBy(factor: Int): (Int) -> Int {return { input: Int -> input * factor }}
In this example, the function multiplyBy returns a lambda that captures the factor variable. When the lambda is executed, it multiplies its input parameter by factor and returns the result.
We can use this function to create two lambdas that multiply their input by different factors:
Kotlin
val double = multiplyBy(2)val triple = multiplyBy(3)
Here, we’re creating two new lambdas by calling multiplyBy with different values for factor. double captures the value 2, while triple captures the value 3.
Now, we can use these lambdas to perform some calculations:
Here, we’re calling double and triple with the input value 5. double(5) returns 10, because 2 * 5 = 10. triple(5) returns 15, because 3 * 5 = 15.
Notice that even though double and triple capture the value of factor when they are created, they can be executed with different input values later. This is because the captured factor variable is stored along with the lambda code, and can be used each time the lambda is executed.
Now, let’s look at an example of capturing a non-final variable. Consider the following function:
This function returns a lambda that increments and returns a local variable count each time it is executed. The count variable is not declared as final, which means that its value can be changed.
We can use this function to create two lambdas that count the number of times they are executed:
Kotlin
val increment1 = counter()val increment2 = counter()
Here, we’re creating two new lambdas by calling counter twice. increment1 and increment2 both capture the same count variable.
Now, let’s execute these lambdas and see what happens:
Here, we’re calling increment1 and increment2 multiple times. The first time each lambda is called, it returns 1, because the initial value of count is 0. The second time each lambda is called, it returns 2, because the value of count has been incremented once.
Notice that both increment1 and increment2 are accessing the same count variable, and that the value of count is being modified each time the lambdas are executed. This is possible because Kotlin creates a special wrapper object for non-final captured variables that allows their values to be modified by the lambda.
Final Variables
When a lambda captures a local variable in Kotlin, the captured variable must be either val or final in Java terminology. This means that the variable must be immutable, or effectively immutable, by the time the lambda captures it. If the variable is mutable, the behavior of the lambda can be unpredictable.
Here’s an example that demonstrates capturing a final variable in Kotlin:
In this example, the outerFunction returns a lambda that captures the final variable message. The lambda prints the value of message when it’s executed. The value of message cannot be modified, so this lambda will always print “Hello, softAai!”.
Non-Final Variables
When a lambda captures a non-final variable in Kotlin, the variable is effectively wrapped in an object that can be modified by the lambda. This allows the lambda to modify the value of the variable even after it has been captured. However, there are some important rules to keep in mind when capturing non-final variables.
Here’s an example that demonstrates capturing a non-final variable in Kotlin:
Kotlin
funouterFunction(): () ->Unit {var counter = 0return { println(counter++) }}val lambda = outerFunction()lambda() // prints "0"lambda() // prints "1"
In this example, the outerFunction returns a lambda that captures the non-final variable counter. The lambda prints the value of counter when it’s executed and increments it by one. The value of counter can be modified by the lambda, so each time the lambda is executed, the value of counter will increase.
However, if you try to modify a captured variable from outside the lambda, you’ll get a compilation error:
This is because the captured variable is effectively wrapped in an object that can only be accessed and modified by the lambda itself. If you want to modify the value of the captured variable from outside the lambda, you’ll need to create a separate variable and update it manually:
In this example, we’ve created a new variable called newCounter inside the lambda and assigned it a value of 10. This allows us to modify the value of the variable without modifying the captured variable.
Capturing mutable variables
The concept of capturing mutable variables in Kotlin lambdas may be a bit confusing, especially for those coming from Java, where only final variables can be captured.
In Kotlin, you can use a trick to capture mutable variables by either declaring an array of one element in which to store the mutable value, orby creating an instance of a wrapper class that stores the reference that can be changed.
To illustrate this, you can create a Ref class with a mutable value property, which can be used to capture a mutable variable in a lambda. Here’s an example of how this can be done:
In this example, a counter variable of type Ref is created with an initial value of 0, and a lambda expression inc is defined to increment the value property of the counter object each time it’s called.
By using the Ref class, you are simulating the capturing of a mutable variable in a lambda, by actually capturing an immutable reference to an instance of the Ref class, which can be mutated to change the value of its value property.
So in Kotlin, you can directly capture a mutable variable like a var by simply referencing it within the lambda. This is because, under the hood, Kotlin creates an instance of a Ref class to capture the mutable variable, and any changes made to it are reflected in the original variable outside the lambda.
Here’s an example:
Kotlin
var counter = 0val inc = { counter++ }
In this example, a counter variable is declared as a var with an initial value of 0, and a lambda expression inc is defined to increment the counter variable each time it’s called.
As here mentioned, the first example with the Ref class shows how the second example works under the hood. When you capture a final variable (val), its value is copied, similar to how it works in Java. However, when you capture a mutable variable (var), Kotlin creates an instance of a Ref class to store the value of the mutable variable, which is then captured as a final variable. The actual value of the mutable variable is then stored in a field of the Ref class, which can be changed from the lambda.
Capturing Objects
When a lambda captures an object in Kotlin, it captures a reference to the object rather than a copy of the object itself. This means that if you modify the object outside the lambda, the changes will be visible inside the lambda.
Here’s an example that demonstrates capturing an object in Kotlin:
In this example, we’ve defined a simple Counter class with a single value property. We’ve also defined an outerFunction that creates a new Counter object and returns a lambda that captures the object. The lambda prints the value of the value property when it’s executed and increments it by one.
If you modify the value property of the Counter object outside the lambda, the changes will be visible inside the lambda:
Kotlin
classCounter {varvalue = 0}funouterFunction(): () ->Unit {val counter = Counter()return { println(counter.value++) }}val lambda = outerFunction()lambda() // prints "0"lambda() // prints "1"lambda() // prints "2"lambda() // prints "3"val counter = Counter()counter.value = 10lambda() // prints "4"
In this example, we’ve created a new Counter object called counter and set its value property to 10. When we call the lambda again, it prints “4”, which shows that the changes to the Counter object are visible inside the lambda as here we deal with another object so changes won’t reflect.
Let’s take another example to understand it clearly.
In this example, we have a Person class with a name and an age property. We also have a lambda expression incrementAge that captures the person object and increments its age property by 1.
When we execute the program, we first print the person object, which has an age of 30. We then execute the incrementAge lambda expression, which modifies the age property of the person object to 31. We print the person object again and see that its age property has been updated to 31.
After that, we modify the age property of the person object outside of the lambda expression, by incrementing it by 1. We print the person object again and see that its age property has been updated to 32.
Finally, we execute the incrementAge lambda expression again, which modifies the age property of the person object to 33. We print the person object one last time and see that its age property has been updated to 33.
What’s happening here is that when we define the incrementAge lambda expression, it captures a reference to the person object, not a copy of it. This means that when we execute the lambda expression and modify the age property of the person object, we are modifying the same object that exists outside of the lambda expression.
So, when we modify the age property of the person object outside of the lambda expression, those changes are visible inside the lambda expression because they are happening to the same object that the lambda expression has captured a reference to.
Conclusion
Capturing variables and objects in Kotlin lambdas can be a powerful tool for writing concise and expressive code. By understanding the rules for capturing final and non-final variables and objects, you can write code that behaves exactly as you expect. However, it’s important to be careful when capturing variables and objects, especially when working with mutable state. By following these guidelines, you can write safe and effective Kotlin code that uses lambdas to their full potential.
When Kotlin was first introduced, developers quickly fell in love with its powerful language features, including sealed classes. However, there was one thing that seemed to be missing: sealed interfaces. At the time, the Kotlin compiler was unable to guarantee that someone couldn’t implement an interface in Java code, which made it difficult to implement sealed interfaces in Kotlin.
But times have changed, and now sealed interfaces are finally available in both Kotlin 1.5 and Java 15 onwards. With sealed interfaces, developers can create more robust and type-safe APIs, just like they could with sealed classes. In this blog post, we’ll take a deep dive into Kotlin sealed interfaces and explore how they can help you build better code. We’ll cover everything from the basics of sealed interfaces to advanced techniques and best practices, so get ready to master this powerful new feature!
Basics of Sealed Interfaces in Kotlin
Like sealed classes, sealed interfaces provide a way to define a closed hierarchy of types, where all the possible subtypes are known at compile time. This makes it possible to create more robust and type-safe APIs, while also ensuring that all the possible use cases are covered.
To create a sealed interface in Kotlin, you can use the sealed modifier before the interface keyword. Here\’s an example:
Kotlin
sealedinterfaceShape {fundraw()}
This creates a sealed interface called Shape with a single method draw(). Note that sealed interfaces can have abstract methods, just like regular interfaces. A sealed interface can only be implemented by classes or objects that are declared within the same file or the same package as the sealed interface itself.
Now, let’s see how we can use a sealed interface in practice. Here’s an example:
In this example, we define a sealed interface named Shape that has a single abstract method named area(). We then define two classes that implement the Shape interface: Circle and Rectangle. Finally, we define a function named calculateArea() that takes an argument of type Shape and returns the area of the shape.
Since the Shape interface is sealed, we cannot implement it outside the current file or package. This means that only the Circle and Rectangle classes can implement the Shape interface.
Sealed interfaces are particularly useful when we want to define a set of related interfaces that can only be implemented by a specific set of classes or objects. For example, we could define a sealed interface named Serializable that can only be implemented by classes that are designed to be serialized.
Subtypes of Sealed Interfaces
To create subtypes of a sealed interface, you can use the sealed modifier before the class keyword, just like with sealed classes. Here\’s an example:
Kotlin
sealedinterfaceShape {fundraw()}sealedclassCircle : Shape {overridefundraw() {println("Drawing a circle") }}sealedclassSquare : Shape {overridefundraw() {println("Drawing a square") }}classRoundedSquare : Square() {overridefundraw() {println("Drawing a rounded square") }}
This creates two sealed classes Circle and Square that implement the Shape interface, as well as a non-sealed class RoundedSquare that extends Square. Note that RoundedSquare is not a sealed class, since it doesn\’t have any direct subtypes.
Using Sealed Interfaces with When Expressions
One of the main benefits of sealed interfaces (and sealed classes) is that they can be used with when expressions to provide exhaustive pattern matching. Here\’s an example:
This function takes a Shape as a parameter and uses a when expression to call the appropriate draw() method based on the subtype of the shape. Note that since Shape is a sealed interface, the when expression is exhaustive, which means that all possible subtypes are covered.
Advanced Techniques and Best Practices
While sealed interfaces provide a powerful tool for creating type-safe APIs, there are some advanced techniques and best practices to keep in mind when working with them.
Interface Delegation
One technique that can be used with sealed interfaces is interface delegation. This involves creating a separate class that implements the sealed interface, and then delegating calls to the appropriate methods to another object. Here’s an example:
Kotlin
sealedinterfaceShape {fundraw()}classCircleDrawer : Shape {overridefundraw() {println("Drawing a circle") }}classSquareDrawer : Shape {overridefundraw() {println("Drawing a square") }}classDrawingTool(privateval shape: Shape) : Shapebyshape {fundraw() { shape.draw()// additional drawing logic here }}
In this example, we’ve created two classes CircleDrawer and SquareDrawer that implement the Shape interface. We\’ve then created a class DrawingTool that takes a Shape as a parameter and delegates calls to the draw() method to that shape. Note that DrawingTool also includes additional drawing logic that is executed after the shape is drawn.
Avoiding Subclassing
Another best practice to keep in mind when working with sealed interfaces is to avoid subclassing whenever possible. While sealed interfaces can be used to create closed hierarchies of subtypes, it’s often better to use composition instead of inheritance to achieve the same effect.
For example, consider the following sealed interface hierarchy:
Kotlin
sealedinterfaceShape {fundraw()}sealedclassCircle : Shape {overridefundraw() {println("Drawing a circle") }}sealedclassSquare : Shape {overridefundraw() {println("Drawing a square") }}classRoundedSquare : Square() {overridefundraw() {println("Drawing a rounded square") }}
While this hierarchy is closed and type-safe, it can also be inflexible if you need to add new types or behaviors. Instead, you could use composition to achieve the same effect:
Kotlin
sealedinterfaceShape {fundraw()}classCircleDrawer : (Circle) -> Unit {overridefuninvoke(circle: Circle) {println("Drawing a circle") }}classSquareDrawer : (Square) -> Unit {overridefuninvoke(square: Square) {println("Drawing a square") }}classRoundedSquareDrawer : (RoundedSquare) -> Unit {overridefuninvoke(roundedSquare: RoundedSquare) {println("Drawing a rounded square") }}classDrawingTool(privateval drawer: (Shape) -> Unit) {fundraw(shape: Shape) {drawer(shape)// additional drawing logic here }}
In this example, we’ve created separate classes for each type of shape, as well as a DrawingTool class that takes a function that knows how to draw a shape. This approach is more flexible than using a closed hierarchy of subtypes, since it allows you to add new shapes or behaviors without modifying existing code.
Extending Sealed Interfaces
Finally, it’s worth noting that sealed interfaces can be extended just like regular interfaces. This can be useful if you need to add new behaviors to a sealed interface without breaking existing code. Here’s an example:
Kotlin
sealedinterfaceShape {fundraw()}interfaceFillableShape : Shape {funfill()}sealedclassCircle : Shape {overridefundraw() {println("Drawing a circle") }}classFilledCircle : Circle(), FillableShape {overridefunfill() {println("Filling a circle") }}
In this example, we’ve extended the Shape interface with a new FillableShape interface that includes a fill() method. We\’ve then created a new FilledCircle class that extends Circle and implements FillableShape. This allows us to add a new behavior (fill()) to the Shape hierarchy without breaking existing code.
Sealed Classes vs Sealed Interfaces
Sealed classes and sealed interfaces are both Kotlin language features that provide a way to restrict the possible types of a variable or a function parameter. However, there are some important differences between the two.
A sealed class is a class that can be extended by a finite number of subclasses. When we declare a class as sealed, it means that all possible subclasses of that class must be declared within the same file as the sealed class itself. This makes it possible to use the subclasses of the sealed class in a when expression, ensuring that all possible cases are handled.
Here’s an example of a sealed class:
Kotlin
sealedclassVehicle {abstractfunaccelerate()}classCar : Vehicle() {overridefunaccelerate() {println("The car is accelerating") }}classBicycle : Vehicle() {overridefunaccelerate() {println("The bicycle is accelerating") }}
In this example, we declare a sealed class called Vehicle. We also define two subclasses of Vehicle: Car and Bicycle. Because Vehicle is sealed, any other possible subclasses of Vehicle must also be declared in the same file.
On the other hand, a sealed interface is an interface that can be implemented by a finite number of classes or objects. When we declare an interface as sealed, it means that all possible implementations of that interface must be declared within the same file or the same package as the sealed interface itself.
Here’s an example of a sealed interface:
Kotlin
sealedinterfaceVehicle {funaccelerate()}classCar : Vehicle {overridefunaccelerate() {println("The car is accelerating") }}objectBicycle : Vehicle {overridefunaccelerate() {println("The bicycle is accelerating") }}
In this example, we declare a sealed interface called Vehicle. We also define two implementations of Vehicle: Car and Bicycle. Because Vehicle is sealed, any other possible implementations of Vehicle must also be declared in the same file or package.
One important difference between sealed classes and sealed interfaces is that sealed classes can have state and behavior, while sealed interfacescan only have behavior. This means that sealed classes can have properties, methods, and constructors, while sealed interfaces can only have abstract methods.
Another difference is that sealed classes can be extended by regular classes or other sealed classes, while sealed interfacescan only be implemented by classes or objects. Sealed classes can also have a hierarchy of subclasses, while sealed interfaces can only have a flat list of implementations.
Advantages
Type Safety: Sealed interfaces allow you to define a closed hierarchy of subtypes, which ensures that all possible use cases are covered. This can help you catch errors at compile time, rather than runtime, making your code more robust and easier to maintain.
Flexibility: Sealed interfaces can be used to define complex hierarchies of subtypes, while still allowing you to add new types or behaviors without breaking existing code. This makes it easier to evolve your code over time, without having to make sweeping changes.
Improved API Design: By using sealed interfaces, you can create more intuitive and expressive APIs that better reflect the domain you are working in. This can help make your code easier to read and understand, especially for other developers who may not be as familiar with your codebase.
Disadvantages
Learning Curve: While sealed interfaces are a powerful feature, they can be difficult to understand and use correctly. It may take some time to become comfortable working with sealed interfaces, especially if you’re not used to working with type hierarchies.
Complexity: As your codebase grows and becomes more complex, working with sealed interfaces can become more difficult. This is especially true if you have a large number of subtypes or if you need to modify the hierarchy in a significant way.
Performance: Because sealed interfaces use type checking at runtime to ensure type safety, they can have a performance impact compared to other approaches, such as using enums. However, this impact is usually negligible for most applications.
Conclusion
Sealed interfaces are a powerful new feature in Kotlin that provide a type-safe way to define closed hierarchies of types. By using sealed interfaces, you can create more robust and flexible APIs, while also ensuring that all possible use cases are covered. Remember to use interface delegation, avoid subclassing, and consider extending sealed interfaces when appropriate to get the most out of this powerful new feature!
Kotlin’s object keyword can be used in a variety of situations, all of which revolve around defining a class and creating an instance of that class at the same time. In this blog post, we’ll explore the different ways in which the object keyword can be used in Kotlin.
The object keyword in Kotlin is a versatile feature that can be used in various situations. The primary idea behind using the object keyword is that it defines a class and creates an instance (or an object) of that class simultaneously. There are three main cases where the object keyword is used:
Object declaration is a way to define a singleton.
Companion objects can contain factory methods and other methods that are related to this class but don’t require a class instance to be called. Their members can be accessed via class name.
Object expression is used instead of Java’s anonymous inner class.
Now we’ll discuss these Kotlin features in detail.
Object Keyword declarations: singletons made easy
This is a way to define a singleton in Kotlin. In Java, this is typically implemented using the Singleton pattern, where a class has a private constructor and a static field holding the only existing instance of the class. In Kotlin, however, the object declaration feature provides first-class language support for defining singletons. An object declaration combines a class declaration and a declaration of a single instance of that class.
For instance, an object declaration can be used to represent the payroll of an organization, where multiple payrolls are unlikely:
Object declarations are introduced with the object keyword. They can contain declarations of properties, methods, initializer blocks, and more. However, constructors (either primary or secondary) are not allowed in object declarations. Unlike instances of regular classes, object declarations are created immediately at the point of definition, not through constructor calls from other places in the code. Therefore, defining a constructor for an object declaration doesn’t make sense.
Inheriting from Classes and Interfaces
Object declarations can inherit from classes and interfaces. This is often useful when you need to implement an interface, but your implementation doesn’t contain any state. For instance, let’s take the java.util.Comparator interface. A Comparator implementation receives two objects and returns an integer indicating which of the objects is greater. Comparators almost never store any data, so you usually need just a single Comparator instance for a particular way of comparing objects. That’s a perfect use case for an object declaration:
Kotlin
objectCaseInsensitiveFileComparator : Comparator<File> {overridefuncompare(file1: File, file2: File): Int {return file1.path.compareTo(file2.path, ignoreCase = true) }}println(CaseInsensitiveFileComparator.compare(File("/User"), File("/user"))) // output is 0
Declaring Objects in a Class
You can also declare objects in a class. Such objects also have just a single instance; they don’t have a separate instance per instance of the containing class. For example, it’s logical to place a comparator:
An object declaration in Kotlin is compiled as a class with a static field holding its single instance, which is always named INSTANCE. To use a Kotlin object from Java code, you access the static INSTANCE field.
here the INSTANCE field has the type CaseInsensitiveFileComparator.
Companion objects: a place for factory methods and static members
Kotlin does not have a static keyword like Java, so it uses different constructs to replace it.
One of the constructs that Kotlin uses to replace static members is package-level functions, which can replace Java’s static methods in many situations. For example, the following Java code:
Kotlin
publicclassUtils {public static int add(int a, int b) {return a + b; }}
can be replaced in Kotlin with a package-level function like this:
Kotlin
package mypackagefunadd(a: Int, b: Int): Int {return a + b}
In most cases, it’s recommended to use package-level functions. However, top-level functions can’t access private members of a class. If you need to write a function that can be called without having a class instance but needs access to the internals of a class, you can write it as a member of an object declaration inside that class. An example of such a function would be a factory method.
In this example, the companion object is used to define two factory methods that can be called on the User class without creating an instance of it. The private constructor of the User class can be called from within the companion object, making it an ideal candidate to implement the Factory pattern.
Another construct that Kotlin uses to replace static members is object declarations. An object declaration creates a singleton instance of a class and can replace static fields and methods in Java. For example, the following Java code:
Kotlin
publicclassSingleton {private static final Singleton INSTANCE = new Singleton();privateSingleton() {}public static Singleton getInstance() {return INSTANCE; }}
can be replaced in Kotlin with an object declaration like this:
Kotlin
objectSingleton {fungetInstance() = this}
In this example, the object declaration Singleton creates a singleton instance of the class and defines a method getInstance() that returns the instance.
One of the objects defined in a class can be marked with a special keyword: companion. If you do that, you gain the ability to access the methods and properties of that object directly through the name of the containing class, without specifying the name of the object explicitly. The resulting syntax looks exactly like static method invocation in Java.
Here’s an example showing the syntax:
Kotlin
classMyClass {companionobject {funmyMethod() {println("Hello from myMethod") } }}// Call myMethod() on the classMyClass.myMethod()
If you need to define functions that can be called on the class itself, like companion-object methods or Java static methods, you can define extension functions on the companion object. For example, imagine that you have a companion object defined like this:
Kotlin
classMyClass {companionobject {funmyMethod() {println("Hello from myMethod") } }}
You can define an extension function on the companion object like this:
Kotlin
funMyClass.Companion.myOtherMethod() {println("Hello from myOtherMethod")}
You can then call myOtherMethod() on the class like this:
Kotlin
MyClass.myOtherMethod()
So companion objects can contain factory methods and other methods related to the class, but they don’t require a class instance to be called. The members of companion objects can be accessed via the class name. Companion objects are declared inside a class using the companion object keyword.
In Kotlin, the object keyword can be used to declare anonymous objects that replace Java\’s use of anonymous inner classes. In this example, let\’s see how to convert a typical Java anonymous inner class—an event listener—to Kotlin using anonymous objects:
The syntax for anonymous objects is similar to object declarations, but the name of the object is omitted. The object expression declares a class and creates an instance of that class, without assigning a name to either the class or the instance. Typically, neither is necessary because the object will be used as a parameter in a function call. However, if necessary, the object can be stored in a variable:
Unlike Java anonymous inner classes that can only extend one class or implement one interface, a Kotlin anonymous object can implement multiple interfaces or no interfaces.
It’s important to note that anonymous objects are not singletons like object declarations. Every time an object expression is executed, a new instance of the object is created.
Anonymous objects are particularly useful when you need to override multiple methods in your anonymous object. However, if you only need to implement a single-method interface (such as Runnable), Kotlin has support for SAM conversion. SAM conversion allows you to convert a function literal to an implementation of an interface with a single abstract method. Therefore, you can implement a single-method interface with a function literal instead of an anonymous object.
The object keyword in Kotlin has several advantages and disadvantages.
Advantages:
Singleton implementation: It allows you to define a Singleton pattern easily and concisely. You can declare a class and its instance at the same time, without the need for a separate class definition or initialization.
Anonymous objects: It enables you to create anonymous objects, which can be used as an alternative to anonymous inner classes in Java. Anonymous objects can implement multiple interfaces and can override methods on the spot, without creating a separate class.
Clean code: It can make your code cleaner and more concise, as it eliminates the need for boilerplate code that is common in Java.
Disadvantages:
Overuse: Using the object keyword extensively in your code can lead to overuse and abuse, making it harder to read and maintain.
Limited functionality: It has limited functionality when compared to a full-fledged class definition. It cannot be inherited or extended, and it cannot have constructors, which limits its usefulness in certain scenarios.
Lack of thread safety: It is not thread-safe by default, which can cause issues in multi-threaded applications. You need to add synchronization code to ensure thread safety.
Overall, the object keyword is a powerful feature in Kotlin that can make your code more concise and eliminate boilerplate code. However, it should be used judiciously to avoid overuse and to ensure thread safety when necessary.
Jetpack Components is a collection of libraries that provide developers with ready-made solutions to common problems encountered when building Android apps. These libraries are designed to work together seamlessly, allowing developers to quickly and easily build high-quality apps. In this article, we will take a closer look at the different Jetpack Components and how they can be used to build better Android apps.
Jetpack Architecture Components
The Architecture Components is a set of libraries that help developers build robust, testable, and maintainable apps. It includes the following components:
ViewModel
The ViewModel component helps manage the UI-related data in a lifecycle-conscious way. It allows data to survive configuration changes such as screen rotations, making it easier to handle data in your app.
LiveData
LiveData is a data holder class that allows you to observe changes in data and update the UI accordingly. It is lifecycle-aware, which means it automatically updates the UI when the app goes into the foreground and stops updates when the app goes into the background.
Room
Room is a SQLite database library that provides an easy-to-use abstraction layer over SQLite. It provides compile-time checks for SQL queries and allows you to easily map Java objects to database tables.
Paging
The Paging library helps you load large data sets efficiently and gradually. It loads data in chunks, making it easier to handle large data sets without consuming too much memory.
WorkManager
WorkManager is a library that makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app is closed or the device is restarted.
Navigation
The Navigation component helps you implement navigation between screens in your app. It provides a consistent and predictable way to navigate between destinations in your app.
UI Components
The UI Components are a set of libraries that help you build beautiful and functional user interfaces. It includes the following components:
Compose UI Toolkit
Compose is a modern UI toolkit that enables developers to build beautiful and responsive user interfaces using a declarative programming model. It simplifies the UI development process by allowing developers to express their UI components in code, using a Kotlin-based DSL.
RecyclerView
RecyclerView is a flexible and efficient way to display large data sets. It allows you to customize the way items are displayed and provides built-in support for animations.
CardView
CardView is a customizable view that displays information in a card-like format. It provides a consistent and attractive way to display information in your app.
ConstraintLayout
ConstraintLayout is a flexible and powerful layout manager that allows you to create complex layouts with a flat view hierarchy. It provides a variety of constraints that allow you to create responsive and adaptive layouts.
ViewPager2
ViewPager2 is an updated version of the ViewPager library that provides better performance and improved API consistency. It allows you to swipe between screens in your app, making it a popular choice for building onboarding experiences.
Material Components
Material Components is a collection of UI components that implement Google’s Material Design guidelines. It provides a consistent look and feel across different Android devices and versions.
Behavior Components
The Behavior Components are a set of libraries that help you implement common app behaviors. It includes the following components:
Download Manager
The Download Manager component makes it easy to download files in your app. It provides a powerful API that allows you to manage downloads, monitor progress, and handle errors.
Media
The Media component provides a set of APIs for working with media files in your app. It allows you to play, record, and manage media files with ease.
Notifications
The Notifications component provides a set of APIs for creating and managing notifications in your app. It allows you to create rich, interactive notifications that engage users.
Sharing
The Sharing component provides a set of APIs for sharing content from your app. It allows you to share text, images, and other types of content with other apps and services.
Foundation Components
The Foundation library provides a set of core utility classes and functions that are used across the other Jetpack libraries. It includes the following components:
AppCompat
AppCompat is a library that provides backwards compatibility for newer Android features on older Android versions. It allows developers to use the latest features of Android while still supporting older versions of the platform.
Android KTX
Android KTX is a set of Kotlin extensions that make writing Android code easier and more concise. It provides extension functions for many of the Android framework classes, making them easier to use and reducing the amount of boilerplate code needed.
Multidex
Multidex is a library that provides support for apps that have a large number of methods, which can cause the 64K method limit to be exceeded. It allows developers to build apps that use more than 64K methods by splitting the app’s classes into multiple dex files.
Test
The Test library provides a set of testing utilities for Android apps, including JUnit extensions, Espresso UI testing, and Mockito mocking framework.
Core
The Core library provides a set of classes and functions that are used across many of the other Jetpack libraries, including utilities for handling lifecycle events, threading, and resource management.
Conclusion
The Jetpack Components are a powerful set of libraries and tools that enable developers to build high-quality Android apps quickly and efficiently. By using these components, developers can focus on building the core features of their apps while relying on well-tested and well-documented solutions for common problems. The Compose UI toolkit takes this a step further, simplifying the UI development process by allowing developers to express their UI components in code. Together, these components make Jetpack a valuable resource for any Android developer.
Kotlin is a powerful and modern programming language that has been gaining popularity in recent years due to its concise and expressive syntax, strong type system, and seamless interoperability with Java. One of the most important features of Kotlin that sets it apart from other languages is its support for constructors, which play a crucial role in creating objects and setting up their initial state.
Constructors in Kotlin are not just a simple way to create objects; they offer a wide range of options and flexibility to customize the object initialization process. In this article, we’ll take an in-depth look at Kotlin constructors and explore the different ways they can be used to create and configure objects, along with some best practices and examples. Whether you’re new to Kotlin or a seasoned developer, this article will provide you with a solid understanding of Kotlin constructors and how to use them to create powerful, flexible classes.
Constructors?
In object-oriented programming, a constructor is a special method that is used to initialize an object’s state when it is first created. A constructor is invoked automatically when an object is created, and it sets the initial values for the object’s properties and executes any initialization code.
Kotlin provides several types of constructors that can be used to create objects. Each constructor has a specific syntax and purpose, and we will discuss each of them in detail below.
Default Constructor
In Kotlin, a default constructor is generated automatically if no constructor is defined explicitly. This default constructor is used to create an instance of the class and initializes the class properties with their default values.
Here is an example of a class with a default constructor:
Kotlin
classPerson {var name: String = ""var age: Int = 0}
In this example, we have defined a class Person with two properties name and age. As we have not defined any constructor, a default constructor is generated automatically.
We can create an instance of this class by simply calling the constructor like this:
Kotlin
val person = Person()
The person object created using the default constructor will have the name property initialized with an empty string and the age property initialized with zero.
Primary Constructor
Kotlin provides two types of constructors for initializing objects: primary and secondary constructors.
The primary constructor is usually the main and concise way to initialize a class, and it is declared inside the class header in parentheses. It serves two purposes: specifying constructor parameters and defining properties that are initialized by those parameters.
Here’s an example of a class with a primary constructor:
Kotlin
classUser(val nickname: String)
In this example, the block of code surrounded by parentheses is the primary constructor, and it has a single parameter named “nickname”. The “val” keyword before the parameter name declares the parameter as a read-only property.
You can also write the same code in a more explicit way using the “constructor” keyword and an initializer block:
In this example, the primary constructor takes a parameter named “_nickname” (with the underscore to distinguish it from the property name), and the “init” keyword introduces an initializer block that assigns the parameter value to the “nickname” property.
The primary constructor syntax is constrained, so if you need additional initialization logic, you can use initializer blocks to supplement it. You can declare multiple initializer blocks in one class if needed.
To elaborate, let’s take the example of a Person class, which has a primary constructor that takes two parameters – name and age. We want to add additional initialization logic to the class, such as checking if the age is valid or not.
Here’s how we can do it using an initializer block:
Kotlin
classPerson(val name: String, val age: Int) {init {if (age < 0) {throwIllegalArgumentException("Age cannot be negative") } }}
In this example, we use an initializer block to add additional initialization logic to the class. The initializer block is introduced with the init keyword, and the code inside it is executed when an instance of the class is created.
The initializer block checks if the age parameter is negative, and if so, throws an IllegalArgumentException with an appropriate error message.
Note that you can declare multiple initializer blocks in a class if needed. For example, suppose we want to initialize some properties based on the constructor parameters. We can add another initializer block to the class like this:
Kotlin
classPerson(val name: String, val age: Int) {val isAdult: Booleaninit {if (age < 0) {throwIllegalArgumentException("Age cannot be negative") } isAdult = age >= 18 }init {println("Person object created with name: $name and age: $age") }}
In this example, we have two initializer blocks. The first one initializes the isAdult property based on the age parameter. The second one simply prints a message to the console.
So you can use initializer blocks to add additional initialization logic to a class, such as checking parameter values, initializing properties based on constructor parameters, or performing other setup tasks. You can declare multiple initializer blocks in a class if needed.
What about “constructor” keyword?
In Kotlin, if the primary constructor has no annotations or visibility modifiers, the constructor keyword can be omitted, and the constructor parameters are placed directly after the class name in parentheses. Here’s an example:
Kotlin
classUser(val nickname: String)
In this case, the constructor keyword is not used explicitly because there are no annotations or visibility modifiers.
However, if you need to add visibility modifiers, annotations, or other modifiers to the constructor, you need to declare it using the constructor keyword. For example:
Kotlin
classUserprivateconstructor(val nickname: String)
In this example, we use the private keyword to make the constructor private, and thus we need to use the constructor keyword to declare it explicitly.
The primary constructor can also include annotations, and other modifiers as needed:
Kotlin
classPerson@Injectconstructor(privateval name: String, var age: Int) {// Class body}
In this example, the primary constructor includes an @Inject annotation and a private visibility modifier for the name property.
So you can omit the constructor keyword in the primary constructor declaration if there are no modifiers or annotations. Otherwise, you need to use it to declare the constructor explicitly.
Default Parameter Values
Kotlin also allows us to provide default values for constructor parameters. This means that we can create an object without providing all the required arguments, as long as the missing arguments have default values.
Kotlin
classPerson(val name: String, val age: Int = 0) {// additional methods and properties can be defined here}
In this example, the Person class has a primary constructor with two parameters: name and age. However, the age parameter has a default value of 0, which means that we can create a Person object with just the name parameter:
Kotlin
val john = Person("John")
In this case, the john variable is assigned a new instance of the Person class with the name property set to “John” and the age property set to 0.
Super Class Initialization
If your class has a superclass, the primary constructor also needs to initialize the superclass. You can do so by providing the superclass constructor parameters after the superclass reference in the base class list.
If you don’t declare any constructors for a class, a default constructor that does nothing will be generated for you.
Kotlin
openclassButton// The default constructor without arguments is generated.
If you inherit the Button class and don’t provide any constructors, you have to explicitly invoke the constructor of the superclass.
Kotlin
classRadioButton: Button()
Here note the difference with interfaces: interfaces don’t have constructors, so if you implement an interface, you never put parentheses after its name in the supertype list.
Private Constructor
If you want to ensure that your class can’t be instantiated by other code, you have to make the constructor private. You can make the primary constructor private by adding the private keyword before the constructor keyword.
Kotlin
classSecretiveprivateconstructor() {}
Here the Secretive class has only a private constructor, the code outside of the class can’t instantiate it.
Secondary Constructor
In addition to the primary constructor, Kotlin allows you to declare secondary constructors. Secondary constructors are optional, and they are defined inside the class body, after the primary constructor and initialization blocks.
A secondary constructor is defined using the constructor keyword followed by parentheses that can contain optional parameters.
Kotlin
openclassView {constructor(ctx: Context) {// some code }constructor(ctx: Context, attr: AttributeSet) {// some code }}
Don’t declare multiple secondary constructors to overload and provide default values for arguments. Instead, specify default values directly
Unlike the primary constructor, the secondary constructor must call the primary constructor, directly or indirectly, using this keyword
Kotlin
classPerson(val name: String, val age: Int) {constructor(name: String) : this(name, 0) // calls the primary constructor with age set to 0}
Super Class Initialization
Here is an example that shows how to define a secondary constructor to initialize the superclass in a different way:
Kotlin
openclassUser(val nickname: String) {// primary constructor}classTwitterUser : User {constructor(email: String) : super(extractNicknameFromEmail(email)) {// secondary constructor }privatefunextractNicknameFromEmail(email: String): String {// some code to extract the nickname from the emailreturn"someNickname" }}
In this example, the TwitterUser class has a secondary constructor that takes an email address as a parameter. The secondary constructor calls the primary constructor of the User class by passing a nickname value that is extracted from the email address.
Note that the secondary constructor is defined using the constructor keyword, followed by the email parameter. The constructor then calls the primary constructor of the superclass (User) using the super keyword with the extracted nickname value as the argument. Finally, the secondary constructor can perform additional initialization logic if needed.
super() or this()
In Kotlin, super() and this() are used to call constructors of the parent/super class and the current class respectively.
In a primary constructor, you can use this to reference another constructor in the same class and super to reference the constructor of the superclass.
The super() is used to call the constructor of the immediate parent/super class of a derived class. It is typically used to initialize the properties or fields defined in the parent/super class. If the parent/super class has multiple constructors, you can choose which one to call by providing the appropriate arguments. For example:
Kotlin
openclassPerson(val name: String) {constructor(name: String, age: Int) : this(name) {// Initialize age property }}classEmployee : Person {constructor(name: String, age: Int, id: Int) : super(name, age) {// Initialize id property }}
In the above example, the Employee class has a secondary constructor that calls the primary constructor of its parent/super class Person with name and age arguments using super(name, age).
On the other hand, the this() function is used to call another constructor of the same class. It can be used to provide multiple constructors with different parameters. If you call another constructor with this(), it must be the first statement in the constructor. For example:
Kotlin
classPerson(val name: String, val age: Int) {constructor(name: String) : this(name, 0) // calls the primary constructor with age set to 0}
In this example, the Person class has a primary constructor that takes both name and age as parameters. It also has a secondary constructor that takes only the name parameter and calls the primary constructor with age set to 0 using the this() keyword. this()is useful when you have multiple constructors in a class and you want to avoid duplicating initialization logic.
Primary Constructor vs Secondary Constructor
Syntax: The primary constructor is defined as part of the class header, inside parentheses, whilesecondary constructors are defined inside the class body and are prefixed with the constructor keyword.
Purpose: The primary constructor is mainly used to initialize the class properties with values passed as parameters, whilesecondary constructors provide an additional way to create objects of a class with different initialization logic.
Constraints: The primary constructor has some constraints such as not allowing code blocks, while secondary constructors can have default parameter values and can contain code blocks.
Invocation: The primary constructor is always invoked implicitly when an object of the class is created, while secondary constructors can be invoked explicitly by calling them with the constructor keyword.
Number: A class can have only one primary constructor, while it can have multiple secondary constructors.
Initialization of superclass:The primary constructor can initialize the superclass by calling the superclass constructor in the class header, while the secondary constructor can initialize the superclass by calling the superclass constructor inside the constructor body with the super keyword.
Kotlin interfaces are a fundamental part of the language and are used extensively in many Kotlin projects. In this blog, we’ll cover all the important aspects of Kotlin interfaces, including their syntax, uses, and examples.
Kotlin Interfaces
In Kotlin, an interface is a type that defines a set of method signatures that a class can implement. An interface can contain abstract methods, default method implementations, and properties. Interfaces are used to define a contract that a class must follow in order to be considered an implementation of the interface.
Syntax
An interface in Kotlin is declared using theinterface keyword, followed by the name of the interface and its body enclosed in curly braces. Here’s an example of a simple interface declaration:
Kotlin
interfaceMyInterface {fundoSomething()}
In this example, the MyInterface interface contains a single method signature, doSomething(). This method is abstract, meaning that it does not have a method body and must be implemented by any class that implements the MyInterface interface.
Interfaces can also include default method implementations, which are method implementations that are provided in the interface itself. Here’s an example
In this example, the MyInterface interface contains two method signatures, doSomething() and doSomethingElse(). The doSomethingElse() method has a default implementation that simply prints a message to the console.
Kotlin interface properties
In Kotlin, interface properties can be declared using the same syntax as regular properties:
Here, property1 has a default value of 42, and property2 has a default value of “default”. The set() method of property2 is overridden to print a message whenever the property is set.
Extending vs Implementing
In Kotlin, both classes and interfaces play a crucial role in object-oriented programming. A class is a blueprint or a template for creating objects, whereas an interface is a collection of abstract methods and properties. An interface can be seen as a contract that a class has to fulfill by implementing all of its abstract methods and properties.
One of the key differences between a class and an interface is that a class can extend only one other class at a time, while an interface can extend any number of other interfaces. This means that an interface can inherit properties and methods from multiple other interfaces.
In Java, we have the extends and the implements keywords for extending a class and implementing interfaces. However, on Kotlin’s side, we don’t have these keywords. Kotlin uses the colon character “:” to indicate both inheritance (extend) and interfaces implementation.
For example, suppose we have two interfaces A and B, and we want to create a new interface C that extends both A and B. In Kotlin, we can achieve this using the following syntax:
Kotlin
interfaceA {funfoo()}interfaceB {funbar()}interfaceC : A, B {funbaz()}
Here, the interface C extends both A and B, and also declares its own method baz.
On the other hand, a class can extend one other class and implement any number of interfaces at the same time. This means that a class can inherit properties and methods from another class, as well as fulfill the contracts of multiple interfaces.
For example, suppose we have a class D that extends another class E and implements two interfaces F and G. In Kotlin, we can achieve this using the following syntax:
Here, the class D extends the class E and implements both interfaces F and G, and also provides the implementation for their respective abstract methods baz and quux.
It is important to note that an interface cannot implement another interface, it can only extend other interfaces. Additionally, when we define an interface that extends another interface, we inherit all of the properties and methods of the parent interface, and we can also define our own abstract methods and properties.
Overall, the difference between extending and implementing is that extending is used to inherit properties and methods from other classes or interfaces while implementing is used to fulfill the contract of an interface by providing implementations for its abstract methods and properties.
Resolving overriding conflicts
When a class implements multiple interfaces that have a property and function with the same name and signature, it may cause a conflict. This is because the class must provide an implementation for that function, but it’s unclear which interface’s implementation should be used. To resolve such conflicts, Kotlin provides below options:
Explicitly specify which implementation to use using the super keyword and the angle brackets notation (<>), which denotes the interface name. For example:
Kotlin
interfaceA {funfoo() { println("A") }}interfaceB {funfoo() { println("B") }}classC : A, B {overridefunfoo() {super<A>.foo() // Use implementation of Asuper<B>.foo() // Use implementation of B }}
2. Define a new implementation that satisfies the requirements of both interfaces. For example:
Kotlin
interfaceA {funfoo() { println("A") }}interfaceB {funfoo() { println("B") }}classC : A, B {overridefunfoo() { println("C") }}
In this case, C defines a new implementation for the foo() function that satisfies the requirements of both interfaces. When foo() is called on an instance of C, the C implementation will be used.
3. If a class implements two interfaces that define a property with the same name, there will be a naming conflict. For example:
Kotlin
interfaceA {valvalue: Int}interfaceB {valvalue: Int}classMyClass : A, B {overridevalvalue: Int = 42}
In the above code, MyClass implements both A and B, which define a variable named value. To resolve this naming conflict, the value property in MyClass must be overridden with the override keyword, and a value must be provided.
If you want to access the variable from both interfaces, you can use the interface name to qualify the variable:
Kotlin
classMyClass : A, B {overridevalvalue: Int = 42funprintValues() {println("A.value = ${A.super.value}") // prints "A.value = 42"println("B.value = ${B.super.value}") // prints "B.value = 42" }}
In the above code, A.super.value and B.super.value are used to access the value property from the respective interfaces.
Default implementation
As we have already seen above, interfaces can also have default implementations for their methods. This means that the implementation of a method can be provided in the interface itself. Any class implementing that interface can then choose to use the default implementation or override it with its own implementation.
In the above example, the Vehicle interface has a default implementation for the stop() method. The Car class implements the Vehicle interface and overrides the start() method. Since it does not override the stop() method, it uses the default implementation provided by the interface.
Delegation
Kotlin interfaces also support delegation. This means that an interface can delegate its method calls to another object. The by keyword is used to delegate method calls to another object.
In the above example, the Driver class implements the Vehicle interface by delegating its method calls to the vehicle object that is passed to it as a constructor parameter. The by keyword is used to delegate the method calls.
SAM conversions
Kotlin interfaces can be used for single abstract method (SAM) conversions. This means that a lambda expression or a function reference can be used wherever an interface with a single abstract method is expected.
Kotlin
interfaceOnClickListener {funonClick()}classButton {funsetOnClickListener(listener: OnClickListener) {// do something with listener }}funmain() {val button = Button()// SAM conversion with lambda expression button.setOnClickListener {println("Button clicked") }// SAM conversion with function reference button.setOnClickListener(::handleClick)}funhandleClick() {println("Button clicked")}
In the above example, the OnClickListener interface has a single abstract method onClick(). The Button class has a method setOnClickListener() that expects an object of the OnClickListener interface. The main() function demonstrates how a lambda expression and a function reference can be used for SAM conversions.
Uses
Kotlin interfaces have a variety of uses, including:
1. Defining APIs
One of the primary uses of Kotlin interfaces is defining APIs. By defining an interface, you can provide a contract for how your code should be used, without providing any implementation details.
For example, imagine you’re building a library that performs some complex calculations. You might define an interface that provides a simple API for performing those calculations:
In this example, we define a Calculator interface with four functions: add(), subtract(), multiply(), and divide(). This interface provides a simple API for performing arithmetic operations.
2. Enforcing Contracts
Another use of Kotlin interfaces is to enforce contracts between different parts of your code. By defining an interface, you can ensure that different parts of your code are compatible with each other.
For example, imagine you’re building an app that allows users to log in. You might define an interface that represents a user session:
In this example, we define a UserSession interface with several properties and functions. This interface enforces a contract between different parts of your code that need to work with user sessions.
3. Polymorphism
A key feature of Kotlin interfaces is polymorphism. By defining an interface, you can create code that can work with objects of different types, as long as they implement the same interface.
For example, imagine you’re building a game that has several different types of enemies. You might define an interface that represents an enemy:
In this example, we define an Enemy interface with two functions: attack() and takeDamage(). This interface allows us to write code that can work with any type of enemy, as long as it implements the Enemy interface.
Examples
Let’s look at a few more examples in action.
1. Defining a callback interface
One common use is defining callback functions. Here’s an example:
In this example, we have defined an OnItemClickListener interface with a single function onItemClick(). The MyAdapter class takes an instance of this interface as a constructor parameter and uses it to handle click events in its view holder.
2. Implementing multiple interfaces
Kotlin interface can be implemented by a single class, allowing for multiple types of behavior to be encapsulated in one object. Here’s an example:
In this example, we have defined two interface Flyable and Swimmable, each with a single function. The Duck class implements both interface, allowing it to exhibit both flying and swimming behavior.
3. Using interface to define contracts
In Kotlin, interfaces can be used to define contracts that classes must adhere to. This allows for more flexible code and promotes loose coupling. Here’s an example:
In this example, we have defined a PaymentProvider interface with a single function processPayment(). The CreditCardPaymentProvider and PayPalPaymentProvider classes both implement this interface to provide payment processing functionality.
The ShoppingCart class takes an instance of PaymentProvider as a constructor parameter and uses it to process payments in its checkout() function. This allows for different payment providers to be used interchangeably, as long as they conform to the PaymentProvider contract.
Kotlin interfaces in Android development
Here are some real-world examples of Kotlin interfaces in Android development.
1. Network Callbacks
One of the most common uses of interface in Android development is for handling callbacks from network operations, such as HTTP requests. For example, you might define a NetworkCallback interface with methods for handling success and error responses, which you can then implement in a class to handle network events. Here’s an example using the Retrofit library:
Interface can also be useful when defining custom views in Android. For example, you might define a CustomViewListener interface with methods for handling user interactions with your custom view, which you can then implement in a class to customize the behavior of your view. Here’s an example:
Kotlin
interfaceCustomViewListener {funonItemSelected(item: MyItem)}classMyCustomView(context: Context, attrs: AttributeSet) : View(context, attrs) {var listener: CustomViewListener? = null// handle user interaction with custom viewprivatefunhandleItemClick(item: MyItem) { listener?.onItemSelected(item) }}// use the custom view in an activityval myCustomView = findViewById<MyCustomView>(R.id.my_custom_view)val listener = object : CustomViewListener {overridefunonItemSelected(item: MyItem) {// handle item selection event }}myCustomView.listener = listener
In each of these examples, interfaces are used to define a contract between different components of the application, allowing for loose coupling and greater flexibility in implementation. By using interface, you can write more modular and reusable code in your Android applications.
Hidden facts about Kotlin interface
1. Kotlin Interface contains companian objects
One hidden fact about Kotlin interface is that they can also contain companion objects. A companion object is an object that is associated with a class or an interface and can be used to define static methods or properties. When defined inside an interface, the companion object is called the companion object of the interface.
Kotlin
interfaceMyInterface {companionobject {funmyFunction() {println("This is a function inside the companion object of MyInterface") } }}funmain() { MyInterface.myFunction()}
In this example, we define a companion object inside the MyInterface interface, which contains a single function called myFunction(). We can call this function from outside the interface by using the name of the interface followed by the name of the companion object and the function.
Companion objects can be useful for providing a way to create instances of the interface, similar to static factory methods in Java. They can also be used to group related functions and constants that are specific to the interface.
It is important to note that, like other members of an interface, the companion object can be implemented by classes that implement the interface. If multiple interfaces contain companion objects with the same name, you must provide an explicit implementation for the conflicting members.
Kotlin
interfaceA {companionobject {funfoo() = println("A companion") }}interfaceB {companionobject {funfoo() = println("B companion") }}classC : A, B {overridefunA.Companion.foo() = println("A implemented")overridefunB.Companion.foo() = println("B implemented")}funmain() {C().A.foo() // Output: A implementedC().B.foo() // Output: B implemented}
2. Kotlin Interface can define Extention Functions
In Kotlin, extension functions can be defined for interfaces, which means that you can add functionality to an existing interface without modifying the interface itself. This is a powerful feature of Kotlin that allows you to extend the functionality of interface without having to modify their source code.
For example, let’s say you have an interface named Clickable that defines a method named onClick(). You can define an extension function for the Clickable interface that provides additional functionality:
Kotlin
interfaceClickable {funonClick()}
Kotlin
funClickable.doubleClick() {onClick()onClick()}
In this example, the doubleClick() function is an extension function for the Clickable interface. It calls the onClick() function twice, effectively simulating a double click.
By allowing extension functions to be defined for interface, Kotlin provides a way to add functionality to existing interface without breaking existing code that relies on those interfaces. This is a major advantage over traditional object-oriented programming languages like Java, which do not allow extension functions to be defined for interface.
However, it is important to note that the use of extension functions can also lead to confusion and make code harder to read if not used carefully. It is also possible to create conflicting extension functions if multiple extension functions with the same name and signature are defined for an interface.
3. About Marker Interface
In Kotlin, marker interfaces are not different from regular interface. A marker interface is simply an interface with no methods or properties, used to mark a class as conforming to a particular contract.
In Java, marker interfaces are used extensively, for example, the Serializable interface is a marker interface, which signals that an object can be serialized. However, in Kotlin, you can achieve the same effect by annotating the class with the @Serializable annotation.
Kotlin provides a few built-in marker interfaces like Cloneable and Serializable, but they are not used as extensively as in Java. In general, it is recommended to use annotations instead of marker interface in Kotlin.
An annotation can be used to mark a class as conforming to a certain contract, and it can also carry additional metadata that can be useful at runtime or compile-time. Annotations can be processed at compile time by tools like the Kotlin Annotation Processor or the Java Annotation Processor, whereas marker interface cannot.
Note that Kotlin does support marker interface, it is generally recommended to use annotations instead, as they provide more flexibility and can be processed by annotation processing tools.
Limitations to using interfaces in Kotlin
There are some limitations to using interface in Kotlin:
Interfaces cannot store state:In Kotlin, interface cannot have any property with a backing field. They can only have abstract properties that must be overridden by classes implementing the interface. However, you can define constant properties in interfaces that do not require a backing field.
Interfaces cannot have constructors: Unlike classes, interface do not have constructors. This means that you cannot instantiate an interface in Kotlin. However, you can implement an interface using a class and instantiate the class instead.
Interfaces cannot have private members: In Kotlin, all members of an interface are public by default. You cannot define private members in an interface.
Interfaces cannot have static members: In Kotlin, interface cannot have static members like in Java. Instead, you can use companion objects to define static members.
Interfaces cannot have final members: Unlike classes, interface in Kotlin cannot have final members. This means that any member of an interface can be overridden by a class implementing the interface.
It is important to keep these limitations in mind when designing your Kotlin application with interfaces.
Advantages of Kotlin interfaces:
Multiple inheritance: Kotlin interfaces allow a class to implement multiple interfaces. This is a significant advantage over Java, which only allows a class to extend one superclass. With interface, you can compose functionality from multiple sources.
Open by default: Kotlin interfaces are open by default, meaning that they can be implemented by any class. This makes it easier to work with interface in Kotlin than in Java, where you must explicitly declare an interface as public and then implement it in a separate class.
Default implementations: In Kotlin, interfaces can provide default implementations for methods. This allows interface to define a common behavior for their methods that can be reused by all implementing classes. This is similar to the default methods in Java 8 interfaces.
Extension functions:Kotlin allows extension functions to be defined for interface. This can be used to add functionality to existing interfaces without modifying the interface itself.
Disadvantages of Kotlin interfaces:
Complexity: While interfaces can be powerful, they can also add complexity to your code. When multiple interface are implemented, it can become difficult to keep track of which methods are being called and from where.
Tight coupling: Interface can lead to tight coupling between classes, which can make it difficult to modify code later on. When a class implements an interface, it is bound to the interface’s API, which can limit the class’s flexibility.
Multiple implementations: If multiple implementations are provided for the same interface method, it can be difficult to determine which implementation will be used. This can result in unexpected behavior and bugs.
Performance: Interface can impact performance, particularly when used extensively in a large codebase. This is due to the additional overhead required to resolve method calls at runtime.
Summary
In summary, Kotlin interface provide a powerful way to define contracts and abstract functionality that can be implemented by classes. They can contain abstract methods, default implementations, and properties. Kotlin interface support multiple inheritance and allow interface to extend other interfaces, but not implement them. Interface can also have companion objects, which can be implemented by classes that implement the interface. Kotlin allows extension functions to be defined for interface, which can add functionality to existing interface without modifying them. Overall, Kotlin interfaces offer many benefits, including flexibility, reusability, and compatibility with Java interfaces. However, they also have some limitations, such as the inability to define static methods or final fields, and the potential for naming conflicts between companion objects.
Monkey patching is a technique used in some programming languages that allows developers to modify or extend the behavior of existing classes at runtime, without the need to modify the original source code. This technique can be useful for adding new functionality to existing code, or for patching bugs or other issues in third-party libraries or frameworks.
In Kotlin, monkey patching refers to the ability to add, modify, or replace methods or properties of an existing class at runtime, without modifying the original class source code. However, unlike in some other languages, such as Python, monkey patching is not a common practice in Kotlin, and it is generally discouraged due to the potential for introducing unexpected behavior and making the code more difficult to maintain.
In Kotlin, monkey patching can be achieved in several ways, including:
Extension functions
Kotlin supports extension functions, which allow you to add new methods to an existing class without modifying the class itself. To define an extension function, you simply declare a function outside of the original class and prefix its name with the class name. For example, to add a new method called “greet” to the String class, you could define the following extension function:
Kotlin
funString.greet() {println("Hello, $this!")}
This function can then be called on any String instance as if it were a method of the original class:
Kotlin
val name = "softAai"name.greet() // prints "Hello, softAai!"
Reflection
Kotlin also supports reflection, which allows you to inspect and modify the properties and methods of a class at runtime. This can be used to monkey patch a class by dynamically adding or modifying its properties or methods. For example, to add a new method called “scream” to the String class using reflection, you could define the following code:
This code would add a new method to the String class called “scream”, which replaces the value of the “value” field with a new character array containing the string “AHHHHH”. However, it’s worth noting that this approach can be complex and error-prone, and should be used with caution.
Proxy classes
Another way to achieve monkey patching in Kotlin is to use proxy classes, which are classes that intercept method calls and modify their behavior at runtime. This can be useful for adding new functionality to existing classes or for patching bugs or other issues in third-party libraries or frameworks. To create a proxy class, you would typically define a new class that implements the same interface or extends the same base class as the original class, and then override the desired methods to add or modify their behavior.
Real-world examples of monkey patching
Here are some real-world examples of monkey patching in Kotlin:
Adding a new method to an existing class using extension functions
Let’s say you’re working on a project that uses a third-party library that provides a “Person” class with some basic functionality, but you need to add a new method to the class that isn’t provided by the library. You could use an extension function to monkey patch the “Person” class and add the new method:
Now, you can call the “greet” method on any instance of the “Person” class, even though it’s not part of the original class definition:
Kotlin
val person = Person("amol", 25)person.greet() // prints "Hello, amol!"
Modifying the behavior of an existing class using reflection
Let’s say you’re working on a project that uses a third-party library that provides a “Math” class with some basic math functions, but you need to modify the behavior of the “sqrt” function to always return a specific value(here 3.0). You could use reflection to monkey patch the “Math” class and modify the “sqrt” method:
Now, whenever the “sqrt” method is called on the “Math” class, it will always return 3.0, regardless of the input value.
Adding new functionality to an existing class using proxy classes:
Let’s say you’re working on a project that uses a third-party library that provides a “Database” class with some basic database functionality, but you need to add a new method to the class that isn’t provided by the library. You could use a proxy class to monkey patch the “Database” class and add the new method:
Now, instead of using the original “Database” class provided by the library, you can use the “DatabaseProxy” class, which extends the original class and adds the new “backup” method:
Kotlin
val database = DatabaseProxy(Database())database.query("SELECT * FROM users")database.backup()
Note that while these examples demonstrate how monkey patching can be achieved in Kotlin, it’s generally recommended to avoid this technique as much as possible, as it can make the code more difficult to understand and maintain. Instead, it’s often better to work with the original source code or use Kotlin’s built-in features, such as extension functions, to add new functionality to existing classes.
One potential issue with monkey patching that should be noted is that it can lead to naming collisions if multiple patches are applied to the same class or library. This can make it difficult to keep track of which patches are being used and can lead to unpredictable behavior. To avoid this, it’s important to use clear and consistent naming conventions for monkey patches and to document them clearly in the codebase.
Another consideration with monkey patching is that it can potentially introduce security vulnerabilities if patches are used to modify sensitive or critical parts of the codebase. It’s important to carefully review and test any monkey patches before applying them in production, and to consider alternative approaches if there are security concerns.
Pros:
Flexibility: Monkey patching allows developers to modify or add functionality to existing classes or libraries without modifying their original source code, which can be especially useful when working with third-party libraries or legacy code.
Rapid prototyping: Monkey patching can also be useful for quickly prototyping or testing new features or functionality without modifying the original source code, allowing developers to experiment and iterate more quickly.
Code reusability: Monkey patching can help reduce code duplication by allowing developers to extend the functionality of existing classes or libraries, rather than writing new code from scratch.
Cons:
Readability and maintainability: Monkey patching can make code more difficult to read and understand, especially for other developers who are not familiar with the codebase. Additionally, since monkey patching modifies existing code at runtime, it can make debugging and maintaining the code more difficult.
Unpredictable behavior: Since monkey patching modifies existing code at runtime, it can lead to unpredictable behavior and unintended consequences. This is especially true when patching code from third-party libraries, as it can be difficult to know how the patch will interact with other parts of the codebase.
Dependency on implementation details: Monkey patching often relies on implementation details of the existing code, such as private methods or fields, which can change between different versions or implementations of the code. This can lead to code that is fragile and difficult to maintain over time.
Conclusion
In general, monkey patching should be used sparingly and only when necessary, as it can have unintended consequences and make code more difficult to maintain. If possible, it’s often better to work with the original source code or use Kotlin’s built-in features, such as extension functions, to add new functionality to existing classes.
Overall, while monkey patching can be a useful technique in some cases, it should be used with caution and with a thorough understanding of its benefits and drawbacks. Developers should carefully consider whether monkey patching is the best approach for their particular use case and should be prepared to document and maintain any patches they create over time.
In Kotlin, an inner class is a class that is nested inside another class, and it has access to the outer class’s properties and methods. Inner classes are useful when you need to group related classes together or when you need to access the outer class’s properties and methods from within the inner class. In this article, we’ll cover all aspects of inner classes in Kotlin.
Declaring an Inner Class
To declare an inner class in Kotlin, you simply use the keyword inner before the class declaration. Here’s an example:
Kotlin
classOuterClass {innerclassInnerClass {// inner class properties and methods }}
In the example above, we have an OuterClass with an inner class called InnerClass.
Relationship between outer and inner class
The relationship between an outer class and an inner class is not an “is-a” relationship, but a “has-a” relationship (composition or aggregation). That means Inner classes can be used when one type of object cannot exist without another type of object. For example, if a university has several departments, the department class can be declared inside the university class since departments cannot exist without the university.
Accessing Outer Class Members
Since an inner class has access to the outer class’s properties and methods, you can access them using this keyword with the name of the outer class. Here’s an example:
Here, we have an OuterClass with a private property called outerProperty. We also have an inner class called InnerClass with a method called printOuterProperty that prints the outerProperty using the this@OuterClass syntax.
Creating an Inner Class Instance
To create an instance of an inner class, you first need to create an instance of the outer class. Here’s an example:
Kotlin
classOuterClass {innerclassInnerClass {// inner class properties and methods }}funmain() {val outer = OuterClass()valinner = outer.InnerClass()}
In the example above, we have an OuterClass with an inner class called InnerClass. We create an instance of the OuterClass called outer and then create an instance of the InnerClass called inner using the outer.InnerClass() syntax.
Types of Inner Classes
1. Nested classes:
Nested classes are declared using the class keyword and are by default static. They can access only the members of the outer class that are static. Here’s an example:
Kotlin
classOuter {privateval outerMember: Int = 1companionobject {constval companionMember = "This is a companion object member." }classNested {funprint() {println("This is a nested class.") } }}
In this example, the Nested class is nested within the Outer class, and the companion object can be accessed without an instance of the Outer class. The companion object can also access the private members of the Outer class. the Nested class is a static nested class that can be accessed without an instance of the outer class. You can create an instance of the Nested class and access the print method like this:
Kotlin
val nested = Outer.Nested()nested.print() // This is a nested class.
And you can access the companion object member like this:
Kotlin
println(Outer.companionMember) // This is a companion object member.
It can only access the outerMember if it is also declared as static.
2. Inner classes:
Inner classes are declared using the inner keyword and are by default non-static. They can access both instance and static members of the outer class. Here’s an example:
Kotlin
classOuter {privateval outerMember: Int = 1innerclassInner {funprint() {println("This is an inner class with access to outerMember: $outerMember.") } }}
In this example, the Inner class is an inner class that can access both instance and static members of the Outer class. You need to create an instance of the Outer class first before you can create an instance of the Inner class:
Kotlin
val outer = Outer()valinner = outer.Inner()inner.print() // This is an inner class with access to outerMember: 1.
Anonymous inner classes:
Anonymous inner classes are unnamed inner classes that are declared and instantiated in a single expression. They are often used for implementing interfaces or extending classes in a concise way. Here’s an example:
In this example, the OnClickListener interface is implemented as an anonymous inner class and passed to the setOnClickListener method of the Button class. This allows us to implement the interface inline without having to define a separate class.
Local inner classes:
Local inner classes are declared inside a block of code, such as a function or a method, and can access both local variables and members of the enclosing class. Here’s an example:
Kotlin
funouterFunction() {val outerMember: Int = 1classInner {funprint() {println("This is a local inner class with access to outerMember: $outerMember.") } }valinner = Inner()inner.print() // This is a local inner class with access to outerMember: 1.}
In this example, the Inner class is a local inner class declared inside the outerFunction function. It can access the outerMember variable of the function, as well as any other members of the outerFunction class.
Use cases of inner classes
Let’s discuss nested and anonymous inner classes in different scenarios to understand their use cases better.
1. Various combinations of nested classes and interfaces
In Kotlin, you can declare nested classes and interfaces inside other classes, and you can use them in various combinations. Here are some examples of different combinations of nested classes and interfaces:
In this example, we declare an interface MyInterface inside the class MyClass. This interface can be implemented by any class, but it is only accessible through an instance of MyClass.
1.b) Nested class inside an interface:
Kotlin
interfaceMyInterface {classNestedClass {fundoSomething() {println("NestedClass is doing something") } }}
In this example, we declare a nested class NestedClass inside the MyInterface. This nested class can be accessed without an instance of MyInterface. We can create an instance of this class and call its doSomething() method as follows:
Kotlin
val nestedClass = MyInterface.NestedClass()nestedClass.doSomething() // prints "NestedClass is doing something"
In this example, we declare an interface MyInterface inside the nested class MyNestedClass. This interface can be implemented by any class, but it is only accessible through an instance of MyClass.MyNestedClass.
In this example, we declare a nested class MyOuterNestedClass inside the MyClass, and a nested class MyInnerNestedClass inside the MyOuterNestedClass. This nested class can be accessed without an instance of MyClass. We can create an instance of this class and call its doSomething() method as follows:
Kotlin
val nestedClass = MyClass.MyOuterNestedClass.MyInnerNestedClass()nestedClass.doSomething() // prints "MyInnerNestedClass is doing something"
2. Anonymous Inner class
2.a)Anonymous Inner class that extends a class:
Kotlin
openclassSuperClass {openfunsayHello() {println("Hello from SuperClass") }}funmain() {val obj = object : SuperClass() {overridefunsayHello() {println("Hello from anonymous inner class") } } obj.sayHello() // Output: Hello from anonymous inner class}
You can create an anonymous inner class that extends a class using the object keyword followed by the class name in parentheses and the body of the class in curly braces.
2.b) Anonymous Inner class that implements an interface:
You can create an anonymous inner class that implements an interface using the object keyword followed by the interface name and the body of the class in curly braces.
2.c) Anonymous Inner class that is defined inside arguments:
You can define an anonymous inner class inside the arguments of a function call or constructor call using the object keyword followed by the class name, interface name, or a generic type and the body of the class in curly braces.
3. Access inner classes from different areas of the outer class
3.a) Access from instance methods or properties of the outer class:
Use this@Outer followed by the dot operator and the name of the inner class.
In all these cases, you can use this keyword to refer to the instance of the current class (inner or outer) and super keyword to refer to the superclass of the current class.
4. Inner Classes with Inheritance
Inner classes can also inherit from other classes. When you inherit from a class in an inner class, you can access the outer class’s properties and methods using thesuperkeyword. Here’s an example:
In this example, the Person class has an open method called greet() which is overridden in the Student class using the override keyword. Within the Student class, an Internship inner class is defined that extends the Job inner class of the Person class.
In the Internship class, the super keyword is used to refer to the printDetails() method of the Job class in the Person class. This allows the Internship class to inherit and extend the behavior of the Job class while also adding its own functionality.
5. Inner classes to improve encapsulation
5.a) Access to Outer Class Properties and Methods
Kotlin
classOuterClass(privateval name: String) {privateval id: Int = 123innerclassInnerClass {funprintOuterName() {println(name) // can access name property of outer class }funprintOuterId() {println(id) // can access id property of outer class } }}
Here InnerClass can access the private name and id properties of the OuterClass. This allows you to keep these properties hidden from the rest of the codebase, while still allowing the InnerClass to use them as needed.
In this example, the Message inner class represents a message to be sent over the network connection. By grouping this related functionality into its own class, you can make your code more organized and easier to understand.
In above example, the ItemSelectionHandler inner class handles item selection events in the ItemListView. By separating this functionality into its own class, you can make your code more modular and easier to test.
5.d) Access Control
Kotlin
classOuterClass {privateval name: String = "John"innerclassInnerClass {privateval age: Int = 30// private to InnerClassfunprintName() {println(name) // can access name property of outer class }funprintAge() {println(age) // can access age property of inner class } }}
In this example, the age property of the InnerClass is private to that class, and cannot be accessed from outside. This helps to prevent unwanted access to certain parts of your codebase and improve the security of your application.
Advantages of Inner Classes in Kotlin
Encapsulation: Inner classes can access private members of the enclosing class, which helps to encapsulate the code and restrict access to certain parts of the code.
Code organization: Inner classes help to organize the code and keep related code together. This makes the code easier to read and understand.
Improved code reuse: Inner classes can be reused in multiple places within the enclosing class, which reduces code duplication and improves code maintainability.
Improved readability: Inner classes can be used to define small, self-contained units of code that are easier to read and understand.
Access to outer class: Inner classes have access to the methods and variables of the outer class, which can be useful in certain situations.
Disadvantages of Inner Classes in Kotlin
Increased complexity: Inner classes can make the code more complex, especially when multiple layers of inner classes are used.
Performance overhead: Inner classes can result in additional memory usage and performance overhead, especially if the inner class is not static.
Tight coupling: Inner classes can create tight coupling between the inner class and the outer class, which can make it difficult to reuse the code in other contexts.
Potential for memory leaks: Inner classes can create memory leaks if they hold references to the outer class, as this can prevent the outer class from being garbage collected.
Name conflicts: Inner classes can have the same name as classes in the outer scope, which can lead to naming conflicts and make the code harder to read and understand.
Clean Architecture and MVVM Architecture are two popular architectural patterns for building robust, maintainable, and scalable Android applications. In this article, we will discuss how to implement Clean Architecture and MVVM Architecture in an Android application using Kotlin. We will cover all aspects of both architectures in-depth and explain how they work together to create a robust application.
Clean Architecture
Clean Architecture is a software design pattern that emphasizes separation of concerns and the use of dependency injection. It divides an application into layers, with each layer having a specific responsibility. The layers include:
Presentation Layer
Domain Layer
Data Layer
The Presentation Layer is responsible for the user interface and interacts with the user. The Domain Layer contains business logic and rules. The Data Layer interacts with external sources of data.
The Clean Architecture pattern is designed to promote testability, maintainability, and scalability. It reduces coupling between different parts of an application, making it easier to modify or update them without affecting other parts of the application.
MVVM Architecture
MVVM stands for Model-View-ViewModel. It is a software design pattern that separates an application into three layers: Model, View, and ViewModel. The Model represents the data and business logic. The View represents the user interface. The ViewModel acts as a mediator between the Model and the View. It exposes data from the Model to the View and handles user input from the View.
MVVM Architecture promotes separation of concerns, testability, and maintainability. It is designed to work with data binding and makes it easy to update the user interface when data changes.
Combining Clean and MVVM Architecture
Clean Architecture and MVVM Architecture can be used together to create a robust, maintainable, and scalable Android application. The Presentation Layer in Clean Architecture corresponds to the View and ViewModel in MVVM Architecture. The Domain Layer in Clean Architecture corresponds to the Model in MVVM Architecture. The Data Layer in Clean Architecture corresponds to the Data Layer in MVVM Architecture.
Implement Clean and MVVM Architecture
Let’s build one demo app to implement Clean and MVVM Architecture. we will create a simple app that displays a list of movies and allows the user to view the details of each movie. We will use the Movie Database API as our data source.
Building this MVVM demo app using Clean Architecture, MVVM, Kotlin, Coroutines, Room, Hilt, Retrofit, Moshi, Flow, and Jetpack Compose.
Set up the project
Create a new project in Android Studio and add the necessary dependencies for MVVM Architecture, such as room, hilt, and ViewModel.
Create initial packages for each layer of Clean Architecture: Presentation, Domain, and Data. Inside each package, create sub-packages for specific functionalities of the layer.
├── data
│ ├── repository
│ └── source
│ ├── local
│ │ ├── datastore
│ │ └── roomdb
│ └── remote
├── di
│ ├── movies
│ └── moviedetails
├── domain
│ ├── model
│ ├── repository
│ └── usecase
└── presentation
├── ui
└── viewmodel
In this hierarchy, we have:
data package which contains the repository and source packages.
The repository package contains classes responsible for fetching data from source and returning it to domain.
The source package contains local and remote packages.
The local package contains classes responsible for accessing data from local data storage, such as datastore and roomdb.
The remote package contains classes responsible for accessing data from remote data storage, such as APIs.
di package which contains the movies and moviedetails packages.
These packages contain classes responsible for dependency injection related to movies and moviedetails modules.
domain package which contains the model, repository, and usecase packages.
The model package contains classes representing the data model of the application.
The repository package contains interfaces defining the methods that the repository classes in data package must implement.
The usecase package contains classes responsible for defining the use cases of the application, by using repository interfaces and returning the result to the presentation layer.
presentation package which contains the ui and viewmodel packages.
The ui package contains classes responsible for the user interface of the application, such as activities, fragments, and views.
The viewmodel package contains classes responsible for implementing the ViewModel layer of the application, which holds data related to the UI and communicates with the usecase layer.
Identify JSON Response
Identify the JSON response from the URL, Before making a network request to the URL, please use your own API Key as mine is an invalid key. Then examine the JSON response using an Online JSON Viewer to identify its structure. Once you have identified the structure of the response, create Kotlin DTOs for the response and place them in the remote package.
DTOs, Entities, and Domain Models:
In our Android application, we will have different types of data models. These models include DTOs, Entities, and Domain Models.
DTOs (Data Transfer Objects) are used to transfer data between different parts of the application. They are typically used to communicate with a remote server or API.
Entities represent the data models in our local database. They are used to persist data in our application.
Domain Models represent the business logic in our application. They contain the logic and rules that govern how data is processed in the application.
By using these different types of models, we can separate our concerns and ensure that each model is responsible for its own functionality. This makes our code more modular and easier to maintain.
Mapper Functions:
In our Android application, we will often need to convert between different types of models. For example, we might need to convert a DTO to an Entity or an Entity to a Domain Model. To do this, we can use Mapper Functions.
Mapper Functions are used to convert data between different models. They take an input model and convert it to an output model. By using Mapper Functions, we can ensure that our code is organized and maintainable, and we can easily convert between different models as needed.
Define DTOs
We can create Kotlin data transfer objects (DTOs) to represent the data and place them into the remote package, as it represents data fetched from a remote data source
To fetch movies from the Movie Database API, we will use Retrofit to define an interface that defines the API endpoints. We will also use Moshi to deserialize the JSON responses into our Movie data class. Here\’s an example of how to define the API interface:
Kotlin
package com.softaai.mvvmdemo.data.source.remoteimport com.softaai.mvvmdemo.data.source.remote.dto.PopularMoviesDtoimport retrofit2.http.GET/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */interfaceMovieApiService {@GET("movie/popular")suspendfungetPopularMovies(): PopularMoviesDtocompanionobject {constval BASE_URL: String = "https://api.themoviedb.org/3/" }}
Here, we are using the @GET annotation to define the API endpoint, and the suspend keyword to indicate that this function should be called from a coroutine. We are also using the deserialized PopularMovieDto data class.
Note → We used PopularMoviesDto data class directly instead of wrapping it in a Response or Resource class. This is because it is assumed that the API response will always contain the expected data structure and any errors in the API call will be handled by catching exceptions, another reason is we are not tightly coupling our app to the API response structure and can modify the response format without affecting the rest of the app.
Define Resource Sealed Class
Resource Sealed Classes are used to represent the state of a request or operation that can either succeed or fail. They allow us to handle different states of an operation, such as loading, success, or error, in a more organized way. Typically, a Resource Sealed Class contains three states:
Loading: When the operation is in progress.
Success: When the operation is successful and data is available.
Error: When the operation fails.
Kotlin
package com.softaai.mvvmdemo.data.source.remote/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */sealedclassResource<T>(valdata: T? = null, val message: String? = null) {classLoading<T>(data: T? = null) : Resource<T>(data)classSuccess<T>(data: T?) : Resource<T>(data)classError<T>(message: String, data: T? = null) : Resource<T>(data, message)}
By using Resource Sealed Classes, we can easily handle different states of an operation in our ViewModel without writing lots of boilerplate code.
Implement Interceptor for network requests
The purpose of the interceptor is to add an API key query parameter to every outgoing network request.
Kotlin
package com.softaai.mvvmdemo.data.source.remoteimport okhttp3.Interceptorimport okhttp3.Response/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */classRequestInterceptor : Interceptor {overridefunintercept(chain: Interceptor.Chain): Response {val originalRequest = chain.request()val newUrl = originalRequest.url .newBuilder() .addQueryParameter("api_key","04a03ff73803441c785b1ae76dbdab9c"//TODO Use your api key this one invalid ) .build()val request = originalRequest.newBuilder() .url(newUrl) .build()return chain.proceed(request) }}
The RequestInterceptor class implements the Interceptor interface provided by the OkHttp library, which allows it to intercept and modify HTTP requests and responses.
In the intercept method, the incoming chain parameter represents the chain of interceptors and the final network call to be executed. The method first retrieves the original request from the chain using chain.request(). It then creates a new URL builder from the original request URL and adds a query parameter with the key \”api_key\” and a specific value to it. Here use your own api_key as existing key is invalid
Next, it creates a new request by calling originalRequest.newBuilder() and setting the new URL with the added query parameter using .url(newUrl). Finally, it calls chain.proceed(request) to execute the modified request and return the response.
Overall, this interceptor helps to ensure that every network request made by the app includes a valid API key, which is required for authentication and authorization purposes.
Implementation of Room
Room is a powerful ORM (Object-Relational Mapping) library that makes it easy to work with a SQLite database in Android. It provides a high-level API for working with database tables, queries, and transactions, as well as support for Flow, LiveData, and RxJava for reactive programming.
Define the Entity
First, we’ll define the MovieEntity class, which represents the movies table in our local database. We annotate the class with @Entity and specify the table name and primary key. We also define the columns using public properties.
Kotlin
package com.softaai.mvvmdemo.data.source.local.roomdb.entityimport androidx.room.ColumnInfoimport androidx.room.Entityimport androidx.room.PrimaryKeyimport com.softaai.mvvmdemo.domain.model.Movie/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */@Entity(tableName = MovieEntity.TABLE_NAME)dataclassMovieEntity(@PrimaryKeyval id: Int,val title: String,val overview: String,@ColumnInfo(name = "poster_url") val posterUrl: String,@ColumnInfo(name = "release_date") val releaseDate: String) {funtoMovie(): Movie {returnMovie( title = title, overview = overview, posterUrl = posterUrl, releaseDate = releaseDate ) }companionobject {constval TABLE_NAME = "movie" }}
We have another entity for API response, which contains a movie entity list
Next, we’ll define the MovieDao interface, which provides the methods to interact with the movies table. We annotate the interface with @Dao and define the query methods using annotations such as @Query, @Insert, and @Delete.
Kotlin
package com.softaai.mvvmdemo.data.source.local.roomdb.daoimport androidx.room.Daoimport androidx.room.Insertimport androidx.room.OnConflictStrategyimport androidx.room.Queryimport com.softaai.mvvmdemo.data.source.local.roomdb.entity.MovieEntity/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */@DaointerfaceMovieDao {@Insert(onConflict = OnConflictStrategy.REPLACE)suspendfuninsertMovieList(movies: List<MovieEntity>)@Query("SELECT * FROM ${MovieEntity.TABLE_NAME}")suspendfungetMovieList(): List<MovieEntity>@Query("DELETE FROM ${MovieEntity.TABLE_NAME}")suspendfundeleteAll()}
Note ->Here also, we are not using any wrappers like Flow, Response, or Resource. The reason behind this is that we are keeping the repository layer decoupled from the data sources (local or remote) and allowing for easier testing and evolution. In this specific case, it is a simple synchronous database operation, as the data is being retrieved from the local database using Room. Room already provides the functionality to perform asynchronous database operations in the background, so we do not need to use any additional wrappers like Flow or Resource. We can simply call the getMovieList() method from a coroutine and retrieve the list of MovieEntity objects.
Define Type Converter
In Room, a type converter is a way to convert non-primitive types (such as Date or custom objects in our case List<MovieEntity>) to primitive types that can be stored in the SQLite database.
To use a type converter in Room, you need to create a class that implements the TypeConverter interface, which has two methods: toType() and fromType(). The toType() method converts a non-primitive type to a primitive type, while the fromType() method converts the primitive type back to the non-primitive type.
To use this TypeConverter, you need to annotate the field or property that needs to be converted with the @TypeConverters annotation, specifying the converter class.
Define the Database
Finally, we’ll define the MovieDatabase class, which represents the entire local database. We annotate the class with @Database and specify the list of entities and version number. We also define a singleton instance of the database using the Room.databaseBuilder method.
Usually In the Domain Layer, we define the interfaces for the Repository and Use Case (In our case, skipped for the Use Case). These interfaces define the methods that will be used to interact with the data layer. The Repository interface defines the methods that will be used to retrieve and save data, while the Use Case interface defines the business logic that will be performed on the data.
We will create a MovieRepository interface that defines the methods for fetching movies:
Kotlin
package com.softaai.mvvmdemo.domain.repositoryimport com.softaai.mvvmdemo.data.source.remote.Resourceimport com.softaai.mvvmdemo.domain.model.Movieimport kotlinx.coroutines.flow.Flow/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */interfaceMovieRepository {fungetPopularMovies(): Flow<Resource<List<Movie>>>}
We are returning a Flow<Resource<List<Movie>>> from the getPopularMovies() function. The Flow will emit the result of the API call asynchronously, and the Resource class will hold either the list of movies or an error.
Implement a Repository interface in the Data Layer
We define interfaces for the Repository and Use Case in the Domain Layer and these interfaces will be implemented in the Data Layer. By separating the interfaces from their implementations, we can easily swap out the data layer implementation if needed. This allows us to easily switch between different data sources, such as a local database or a remote API, without having to modify the business logic layer.
In our example, we will create an implementation of the MovieRepository interface that uses Retrofit and Moshi to fetch the popular movies:
Kotlin
package com.softaai.mvvmdemo.data.repositoryimport com.softaai.mvvmdemo.data.source.local.roomdb.dao.MovieDaoimport com.softaai.mvvmdemo.data.source.local.roomdb.dao.PopularMoviesDaoimport com.softaai.mvvmdemo.data.source.remote.MovieApiServiceimport com.softaai.mvvmdemo.data.source.remote.Resourceimport com.softaai.mvvmdemo.domain.model.Movieimport com.softaai.mvvmdemo.domain.repository.MovieRepositoryimport kotlinx.coroutines.flow.Flowimport kotlinx.coroutines.flow.flowimport retrofit2.HttpExceptionimport java.io.IOException/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */classMovieRepositoryImplconstructor(privateval movieApiService: MovieApiService,privateval popularMoviesDao: PopularMoviesDao,privateval movieDao: MovieDao) : MovieRepository {overridefungetPopularMovies(): Flow<Resource<List<Movie>>> = flow {emit(Resource.Loading())try {fetchAndInsertPopularMovies(movieApiService, popularMoviesDao, movieDao) } catch (e: HttpException) {emit( Resource.Error( message = "Oops, something went wrong!" ) ) } catch (e: IOException) {emit( Resource.Error( message = "Couldn't reach server, check your internet connection." ) ) }// single source of truth we will emit data from db only and not directly from remoteemit(Resource.Success(getPopularMoviesFromDb(movieDao))) }privatesuspendfunfetchAndInsertPopularMovies( movieApiService: MovieApiService, popularMoviesDao: PopularMoviesDao, movieDao: MovieDao ) {val remotePopularMovies = movieApiService.getPopularMovies() popularMoviesDao.insertPopularMovies(remotePopularMovies.toPopularMoviesEntity()) movieDao.insertMovieList(remotePopularMovies.results.map { it.toMovieEntity() }) //now insert newly fetched data to db }privatesuspendfungetPopularMoviesFromDb(movieDao: MovieDao): List<Movie> {val newPopularMovies = movieDao.getMovieList().map { it.toMovie() }return newPopularMovies }}
Here, we are using the flow builder from the Kotlin coroutines library to emit the result of the API call asynchronously. We are also using the catch operator to catch any exceptions that might occur during the API call. If there is an error, we emit the error wrapped in the Resource.Error class.
Implement Use Case
I skipped implementing the Use Case in the Data Layer, such as the Repository, and instead implemented it directly in the Domain Layer for this small assignment. However, in a bigger project, it is important to implement it properly in the Data Layer.
Kotlin
package com.softaai.mvvmdemo.domain.usecaseimport com.softaai.mvvmdemo.data.source.remote.Resourceimport com.softaai.mvvmdemo.domain.model.Movieimport com.softaai.mvvmdemo.domain.repository.MovieRepositoryimport kotlinx.coroutines.flow.Flow/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */classGetPopularMovies(privateval movieRepository: MovieRepository) {operatorfuninvoke(): Flow<Resource<List<Movie>>> {return movieRepository.getPopularMovies() }}
The GetPopularMovies class is a use case class in the domain layer that provides a way to retrieve a list of popular movies from the MovieRepository. By using this class, we can easily retrieve the list of popular movies by calling its invoke() method an operator function, which returns a Flow. We can then collect the items emitted by the Flow and handle the different states of the data using the Resource class.
Add Hilt Modules for Dependency Injection
Hilt is a dependency injection framework that makes it easy to manage dependencies in Android apps. It is built on top of Dagger, a popular dependency injection library, and provides a simpler, more streamlined API for configuring and injecting dependencies.
To inject dependencies into our ViewModel and Repository, we’ll use Hilt for Dependency Injection. Since we have already added the Hilt dependency in the gradle file, we can now directly annotate our Application class with @HiltAndroidApp:
Kotlin
package com.softaai.mvvmdemoimport android.app.Applicationimport dagger.hilt.android.HiltAndroidApp/** * Created by amoljp19 on 4/19/2023. * softAai Apps. */@HiltAndroidAppclassMvvmDemoApp : Application() {}
Define Hilt modules
Create a Kotlin object for each module and annotate it with @Module. In each module, define one or more provider methods that create instances of your dependencies and annotate them with @Provides.
This is a Hilt module called MoviesNetworkModule, which is used for providing dependencies related to network communication with the MovieApiService. The module is annotated with @Module and @InstallIn(SingletonComponent::class), which means that it will be installed in the SingletonComponent and has the scope of the entire application.
The module provides the following dependencies:
OkHttpClient: This dependency is provided by a method called provideOkHttpClient, which returns an instance of OkHttpClient that is built with RequestInterceptor and HttpLoggingInterceptor.
MovieApiService: This dependency is provided by a method called provideRetrofitService, which takes an instance of OkHttpClient as a parameter and returns an instance of MovieApiService. This method builds a Retrofit instance using MoshiConverterFactory for JSON parsing and the provided OkHttpClient, and creates a MovieApiService instance using the Retrofit.create method.
The @Singleton annotation is used on both provideOkHttpClient and provideRetrofitService methods, which means that Hilt will only create one instance of each dependency and provide it whenever it is needed.
By using these @Provides methods, we can provide these dependencies to any component in our app by simply annotating the constructor of that component with @Inject.
MoviesDatabaseModule
Kotlin
package com.softaai.mvvmdemo.di.moviesmoduleimport android.app.Applicationimport com.softaai.mvvmdemo.data.source.local.roomdb.MovieDatabaseimport dagger.Moduleimport dagger.Providesimport dagger.hilt.InstallInimport dagger.hilt.components.SingletonComponentimport javax.inject.Singleton/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */@Module@InstallIn(SingletonComponent::class)classMoviesDatabaseModule {@Singleton@ProvidesfunprovideDatabase(application: Application) = MovieDatabase.getDatabase(application)@Singleton@ProvidesfunprovidePopularMoviesDao(database: MovieDatabase) = database.getPopularMoviesDao()@Singleton@ProvidesfunprovideMovieDao(database: MovieDatabase) = database.getMovieDao()}
Here we have defined a Hilt module called MoviesDatabaseModule which is annotated with @Module and @InstallIn(SingletonComponent::class). This means that this module will be installed in the SingletonComponent which has the scope of the entire application. By using these @Provides methods, we can provide these dependencies to any component in our app by simply annotating the constructor of that component with @Inject.
For example, if we want to use PopularMoviesDao in our MovieRepository, we can simply annotate the constructor of MovieRepository with @Inject and pass PopularMoviesDao as a parameter:
By doing this, Hilt will automatically provide the PopularMoviesDao, MovieDao, and MovieApiService objects to our MovieRepository whenever it is needed.
This is a Dagger Hilt module for providing the MovieRepository implementation to the app. The module is annotated with @Module and @InstallIn(SingletonComponent::class) which means that the MovieRepository will have a singleton scope throughout the app.
The @Provides method is defined to provide the MovieRepositoryImpl instance. This method takes three parameters: movieApiService of type MovieApiService, popularMoviesDao of type PopularMoviesDao, and movieDao of type MovieDao. These dependencies are injected into the constructor of MovieRepositoryImpl to create its instance.
MoviesUseCaseModule
Kotlin
package com.softaai.mvvmdemo.di.moviesmoduleimport com.softaai.mvvmdemo.domain.repository.MovieRepositoryimport com.softaai.mvvmdemo.domain.usecase.GetPopularMoviesimport dagger.Moduleimport dagger.Providesimport dagger.hilt.InstallInimport dagger.hilt.components.SingletonComponentimport javax.inject.Singleton/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */@Module@InstallIn(SingletonComponent::class)classMoviesUsecaseModule {@Provides@SingletonfunprovideGetPopularMoviesUseCase(repository: MovieRepository): GetPopularMovies =GetPopularMovies(repository)}
The GetPopularMovies use case by injecting the MovieRepository. The module is annotated with @InstallIn(SingletonComponent::class) which means it will be installed in the SingletonComponent of the application.
The provideGetPopularMoviesUseCase method is annotated with @Provides and @Singleton, indicating that it provides a singleton instance of the GetPopularMovies use case.
The repository parameter of the method is injected via constructor injection, as it is declared as a dependency of the GetPopularMovies constructor. The MovieRepository is provided by the MoviesRepositoryModule which is also installed in the SingletonComponent.
Define the ViewModel
Now, we can define the ViewModel that will be used to expose the movie data to the UI. We will create a MoviesViewModel class that extends the ViewModel class from the Android Architecture Components library:
This is the implementation of the MoviesViewModel, which is responsible for fetching and providing the list of popular movies to the UI layer. It uses the GetPopularMovies use case to fetch the data from the repository and updates the UI state based on the result of the operation.
Kotlin
package com.softaai.mvvmdemo.presentation.viewmodelimport com.softaai.mvvmdemo.domain.model.Movie/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */dataclassMovieUiState(val moviesList: List<Movie> = emptyList(),val isLoading: Boolean = false)
The @HiltViewModel annotation is used to inject dependencies into a ViewModel using Hilt. When a ViewModel is annotated with @HiltViewModel, Hilt generates a factory for the ViewModel and provides dependencies to the ViewModel via this factory. This way, the ViewModel can easily access dependencies, such as use cases or repositories, without the need to manually create and inject them.
The ViewModel uses a mutableStateOf() function to create a state object that can be updated from anywhere in the ViewModel. The state object is exposed as an immutable State object to the UI layer, which can observe it and update the UI accordingly.
The ViewModel also uses the viewModelScope to launch a coroutine that executes the use case, and observes the result of the operation using the onEach operator. Based on the result, the ViewModel updates the UI state accordingly, indicating whether the data is being loaded, whether it has been loaded successfully, or whether an error has occurred.
Define the Compose UI
First we define a MovieItem composable that displays a single movie item in a row. We are using CoilImage from the Coil library to display the movie poster image, and Row and Column composable functions to create the layout.
A composable function called CoilImage, displays an image using Coil library in Jetpack Compose. The function takes a String parameter called imageUrl which is the URL of the image to be displayed.
Finally, we will create a MoviesListScreen composable function that displays a list of popular movies using a LazyColumn.
Kotlin
package com.softaai.mvvmdemo.presentation.ui.composeimport androidx.compose.foundation.layout.PaddingValuesimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.runtime.Composableimport androidx.compose.ui.Modifierimport androidx.compose.ui.unit.dpimport androidx.hilt.navigation.compose.hiltViewModelimport com.softaai.mvvmdemo.presentation.viewmodel.MoviesViewModel/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */@ComposablefunMovieListScreen(moviesViewModel: MoviesViewModel = hiltViewModel()) {val state = moviesViewModel.state.valueLazyColumn( Modifier.fillMaxSize(), contentPadding = PaddingValues(bottom = 16.dp) ) {items(state.moviesList.size) { i ->MovieItem(movie = state.moviesList[i], onItemClick = {}) } }}
The Composable function MovieListScreen which takes a MoviesViewModel as a parameter and sets its default value using the hiltViewModel() function provided by the Hilt library that allows you to retrieve a ViewModel instance that is scoped to the current Compose component. This is useful because it allows you to inject dependencies directly into your ViewModel using the Hilt dependency injection system.
By using hiltViewModel() instead of creating a new instance of the MoviesViewModel class manually, you ensure that the instance of the MoviesViewModel used in the MovieListScreen composable is the same instance that is injected by Hilt into the ViewModel.
Inside the function, it gets the current state of the ViewModel using moviesViewModel.state.value and stores it in a variable called state.
It then creates a LazyColumn with Modifier.fillMaxSize() and a content padding of PaddingValues(bottom = 16.dp). Inside the LazyColumn, it creates a list of items using the items function, which iterates over the state.moviesList and creates a MovieItem for each movie.
The MovieItem composable is passed the movie object from the current iteration, and an empty lambda function onItemClick (which could be used to handle clicks on the item).
Putting it all together
Now, we can put all the pieces together in our MainActivity, which is annotated with the @AndroidEntryPoint annotation. This annotation is part of the Hilt library, and it allows Hilt to generate a component for the activity and provide dependencies to its fields and methods.
Kotlin
package com.softaai.mvvmdemoimport android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.material.MaterialThemeimport androidx.compose.material.Surfaceimport androidx.compose.material.Textimport androidx.compose.runtime.Composableimport androidx.compose.ui.Modifierimport androidx.compose.ui.tooling.preview.Previewimport com.softaai.mvvmdemo.presentation.ui.compose.MovieListScreenimport com.softaai.mvvmdemo.presentation.ui.theme.MVVMDemoThemeimport dagger.hilt.android.AndroidEntryPoint@AndroidEntryPointclassMainActivity : ComponentActivity() {overridefunonCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {MVVMDemoTheme {// A surface container using the 'background' color from the themeSurface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background ) {MovieListScreen() } } } }}
Inside the onCreate method, the setContent method is used to set the main content of the activity. In this case, the content is the MovieListScreen composable function, which displays a list of movies.
Note — I have provided proper guidance on how to display the list of movies. Now, you can continue building the movie details screen by following similar patterns as discussed earlier. If you face any issues or need any further assistance, feel free to ask me.
Conclusion
In this article, we have demonstrated how to build an Android app using Clean Architecture, MVVM, Kotlin, Room, Hilt, Retrofit, Moshi, Flow, and Jetpack Compose. We have covered all aspects of the app development process, including defining the data model, implementing the repository layer, defining the ViewModel, and defining the UI. By following these best practices, we can create robust and maintainable Android apps that are easy to test and evolve over time.
Note — I have provided proper guidance on how to display the list of movies. I now expect you to complete the remaining work by following the guidelines.