Amol Pawar

State Hoisting in Jetpack Compose

State Hoisting in Jetpack Compose: Best Practices for Scalable Apps

When building Android apps with Jetpack Compose, state management is one of the most important pieces to get right. If you don’t handle state properly, your UI can become messy, tightly coupled, and hard to scale. That’s where State Hoisting in Jetpack Compose comes in.

In this post, we’ll break down what state hoisting is, why it matters, and how you can apply best practices to make your Compose apps scalable, maintainable, and easy to debug.

What Is State Hoisting in Jetpack Compose?

In simple terms, state hoisting is the process of moving state up from a child composable into its parent. Instead of a UI component directly owning and mutating its state, the parent holds the state and passes it down, while the child only receives data and exposes events.

This separation ensures:

  • Reusability: Components stay stateless and reusable.
  • Single Source of Truth: State is managed in one place, reducing bugs.
  • Scalability: Complex UIs are easier to extend and test.

A Basic Example of State Hoisting

Let’s say you have a simple text field. Without state hoisting, the child manages its own state like this:

Kotlin
@Composable
fun SimpleTextField() {
    var text by remember { mutableStateOf("") }

    TextField(
        value = text,
        onValueChange = { text = it }
    )
}

This works fine for small apps, but the parent composable has no control over the value. It becomes difficult to coordinate multiple composables.

Now let’s apply state hoisting:

Kotlin
@Composable
fun SimpleTextField(
    text: String,
    onTextChange: (String) -> Unit
) {
    TextField(
        value = text,
        onValueChange = onTextChange
    )
}

And in the parent:

Kotlin
@Composable
fun ParentComposable() {
    var text by remember { mutableStateOf("") }

    SimpleTextField(
        text = text,
        onTextChange = { text = it }
    )
}

Here’s what changed

  • The parent owns the state (text).
  • The child only displays the state and sends updates back via onTextChange.

This is the core idea of State Hoisting in Jetpack Compose.

Why State Hoisting Matters for Scalable Apps

As your app grows, different UI elements will need to communicate. If each composable owns its own state, you’ll end up duplicating data or creating inconsistencies.

By hoisting state:

  • You centralize control, making it easier to debug.
  • You avoid unexpected side effects caused by hidden internal state.
  • You enable testing, since state management is separated from UI rendering.

Best Practices for State Hoisting in Jetpack Compose

1. Keep Composables Stateless When Possible

A good rule of thumb: UI elements should be stateless and only care about how data is displayed. The parent decides what data to provide.

Example: A button shouldn’t decide what happens when it’s clicked — it should simply expose an onClick callback.

2. Use remember Wisely in Parents

State is usually managed at the parent level using remember or rememberSaveable.

  • Use remember when state only needs to survive recomposition.
  • Use rememberSaveable when you want state to survive configuration changes (like screen rotations).
Kotlin
var text by rememberSaveable { mutableStateOf("") }

3. Follow the Unidirectional Data Flow Pattern

Compose encourages Unidirectional Data Flow (UDF):

  1. Parent owns state.
  2. State is passed down to child.
  3. Child emits events back to parent.

This clear flow makes apps predictable and avoids infinite loops or messy side effects.

4. Keep State Close to Where It’s Used, But Not Too Close

Don’t hoist all state to the top-level of your app. That creates unnecessary complexity. Instead, hoist it just far enough up so that all dependent composables can access it.

For example, if only one screen needs a piece of state, keep it inside that screen’s parent composable rather than in the MainActivity.

5. Use ViewModels for Shared State Across Screens

For larger apps, when multiple screens or composables need the same state, use a ViewModel.

Kotlin
class LoginViewModel : ViewModel() {
    var username by mutableStateOf("")
        private set

    fun updateUsername(newValue: String) {
        username = newValue
    }
}

Then in your composable:

Kotlin
@Composable
fun LoginScreen(viewModel: LoginViewModel = viewModel()) {
    SimpleTextField(
        text = viewModel.username,
        onTextChange = { viewModel.updateUsername(it) }
    )
}

This pattern keeps your UI clean and separates business logic from presentation.

Common Mistakes to Avoid

  • Keeping state inside deeply nested children: This makes it impossible to share or control at higher levels.
  • Over-hoisting: Don’t hoist state unnecessarily if no other composable needs it.
  • Mixing UI logic with business logic: Keep state handling in ViewModels where appropriate.

Conclusion

State Hoisting in Jetpack Compose is more than just a coding pattern — it’s the backbone of building scalable, maintainable apps. By lifting state up, following unidirectional data flow, and keeping components stateless, you set yourself up for long-term success.

To summarize:

  • Keep state in the parent, not the child.
  • Pass data down, send events up.
  • Use ViewModels for shared or complex state.

By applying these best practices, you’ll build apps that are not only functional today but also easy to scale tomorrow.

Symbolic AI

The Evolution of Artificial Intelligence: Why Symbolic AI Still Matters in Today’s AI Landscape

Artificial Intelligence (AI) has been in constant evolution for more than five decades, transforming from early symbolic reasoning systems to the powerful neural networks we use today. While much of the spotlight now shines on machine learning and deep learning, understanding the roots of AI is essential for grasping its current capabilities — and limitations.

At the heart of AI’s history lies Symbolic AI, often referred to as “good old-fashioned AI.” Though sometimes overshadowed by modern techniques, symbolic methods remain relevant, powering everything from simple decision-making systems to advanced robotics. 

In this article, we’ll explore the origins of Symbolic AI, how it works, its strengths and weaknesses, and why it continues to hold value in today’s AI-driven world.

What Is Symbolic AI?

Symbolic AI is the practice of encoding human knowledge into explicit rules that a machine can follow. Instead of learning patterns from massive datasets (like modern neural networks do), symbolic AI relies on logical reasoning structures such as:

“If X = Y and Y = Z, then X = Z.”

From the 1950s through the 1990s, symbolic approaches dominated AI research and applications. Even though they’ve been largely supplanted by machine learning, symbolic methods are still actively used in:

  • Control systems (e.g., thermostats, traffic lights)
  • Decision support (e.g., tax calculation systems)
  • Industrial automation
  • Robotics and expert systems

The Building Blocks of Symbolic AI

1. Expert Systems

Expert systems simulate the decision-making abilities of human specialists. A domain expert encodes knowledge into a set of if-then-else rules, which the computer uses to reach conclusions.

For example, an early medical expert system might include rules like:

  • IF patient has a fever AND sore throat → THEN possible diagnosis = strep infection.

The advantages of expert systems include:

  • Transparency: Easy to understand and debug.
  • Human-in-the-loop: Directly reflects expert knowledge.
  • Customizability: Can be updated as rules evolve.

Limitations: Expert systems struggle in domains where knowledge is vast and constantly changing. For instance, simulating a doctor’s full expertise would require millions of rules and exceptions — quickly becoming unmanageable.

Best-fit use case: Domains with stable rules and clear variables, such as calculating tax liability based on income, allowances, and levies.

2. Fuzzy Logic

Unlike expert systems that rely on binary answers (true/false), fuzzy logic allows for degrees of truth — any value between 0 and 1. This makes it well-suited for handling uncertainty and nuanced variables.

Example:
 Instead of saying “Patient has a fever if temperature > 37°C”, fuzzy logic assigns a truth value. A 37.5°C fever might be 0.6 “true,” factoring in age, time of day, or other conditions.

Practical applications of fuzzy logic include:

  • Consumer electronics: Cameras adjusting brightness automatically.
  • Finance: Stock trading systems balancing complex market conditions.
  • Automation: Household appliances like washing machines or air conditioners adapting to usage patterns.

The Strengths and Weaknesses of Symbolic AI

Strengths:

  • Transparent decision-making process.
  • Effective in structured, rule-based environments.
  • Reliable in repetitive, well-defined tasks.

Weaknesses:

  • Requires heavy human intervention for updates and improvements.
  • Struggles with dynamic environments where variables and rules change frequently.
  • Cannot match the adaptability of modern machine learning systems.

This is why Symbolic AI is affectionately known as “Good Old-Fashioned AI” (GOFAI) — useful, reliable, but limited compared to today’s deep learning technologies.

Why Symbolic AI Still Matters Today

Despite its limitations, Symbolic AI hasn’t disappeared. In fact, it plays a crucial role when explainability and transparency are required — two areas where neural networks often fall short.

For example:

  • In medical decision support systems, doctors benefit from clear, rule-based outputs they can verify.
  • In legal and financial systems, symbolic AI ensures compliance with codified regulations.
  • In safety-critical applications (like aviation control), rules-based AI adds a layer of predictability and trust.

In many industries, hybrid approaches are now emerging — combining symbolic reasoning with machine learning to achieve both transparency and adaptability.

Conclusion

The journey of AI from symbolic reasoning to artificial neural networks shows just how far the field has advanced. Yet, symbolic AI remains a cornerstone, offering clarity, reliability, and control in areas where modern machine learning struggles.

Key takeaway: While deep learning dominates headlines, Symbolic AI continues to provide practical, trustworthy solutions in rule-driven environments. For the future, expect to see more hybrid systems that merge the best of both worlds — symbolic reasoning for transparency and neural networks for adaptability.

FAQs About Symbolic AI

Q1. What is the main difference between Symbolic AI and Machine Learning?
 Symbolic AI uses explicit rules programmed by humans, while machine learning relies on algorithms that learn from large datasets.

Q2. Is Symbolic AI still used today?
 Yes. It’s widely used in decision support systems, automation, control systems, and industries that require transparency and compliance.

Q3. What are the advantages of fuzzy logic over traditional expert systems?
 Fuzzy logic handles uncertainty better by assigning “degrees of truth,” making it more flexible for real-world scenarios.

Q4. Why is Symbolic AI called ‘Good Old-Fashioned AI’?
 Because it was the dominant approach in the early decades of AI research (1950s–1990s) and is still respected for its reliability, despite being overtaken by newer methods.

Q5. Will Symbolic AI ever become obsolete?
 Unlikely. While machine learning dominates today, Symbolic AI’s strength in transparency and rule-based decision-making ensures it will remain valuable, especially in regulated or safety-critical industries.

Building Type-Safe HTML with Kotlin DSLs

Building Type-Safe HTML with Kotlin DSLs: A Practical Guide

When working with HTML generation in Kotlin, developers often face a choice: write raw HTML as text or build it programmatically. Thanks to domain-specific languages (DSLs), Kotlin offers a clean, type-safe, and flexible way to construct HTML directly in code.

In this guide, we’ll explore how to use Kotlin’s internal DSLs — with examples from the kotlinx.html library — to generate HTML efficiently. We’ll also highlight why DSLs are more than just a convenience: they add type safety, readability, and maintainability to your codebase.

What Is an Internal DSL?

A Domain-Specific Language (DSL) is a mini-language tailored for a specific task. An internal DSL is built within an existing language (like Kotlin), leveraging its syntax and features to make the new language feel natural and intuitive.

For HTML, this means you can write Kotlin code that looks and feels like HTML, while still enjoying the benefits of the Kotlin compiler.

Example: Creating a Simple Table

Let’s start with a basic example using kotlinx.html:

Kotlin
import kotlinx.html.*
import kotlinx.html.stream.createHTML

fun createSimpleTable(): String = createHTML().table {
    tr {
        td { +"cell" }
    }
}

This generates the following HTML:

HTML
<table>
    <tr>
        <td>cell</td>
    </tr>
</table>

At first glance, this might seem like extra work compared to just writing raw HTML. But there are key advantages.

Why Build HTML with Kotlin Code Instead of Plain Text?

Here are the main reasons developers prefer DSLs for HTML generation:

Type Safety

  • The compiler enforces proper nesting. For example, a <td> can only appear inside a <tr>. If you misuse tags, your code won’t compile—catching errors early.

Dynamic Content Generation

  • Because it’s Kotlin code, you can loop, conditionally render, or dynamically build elements.

Cleaner, More Expressive Code

  • DSLs reduce boilerplate and improve readability. Your HTML structure is represented directly in Kotlin’s syntax.

Example: Building Dynamic HTML from Data

Here’s a slightly more advanced example where we generate a table dynamically from a Map:

Kotlin
import kotlinx.html.*
import kotlinx.html.stream.createHTML

fun createAnotherTable(): String = createHTML().table {
    val numbers = mapOf(1 to "one", 2 to "two")
    for ((num, string) in numbers) {
        tr {
            td { +"$num" }
            td { +string }
        }
    }
}

This produces:

HTML
<table>
    <tr>
        <td>1</td>
        <td>one</td>
    </tr>
    <tr>
        <td>2</td>
        <td>two</td>
    </tr>
</table>

Instead of manually writing repetitive markup, the loop handles it for you — making the code concise, flexible, and maintainable.

Beyond HTML: DSLs for XML and More

Although our examples focus on HTML, the same approach applies to other structured languages like XML. Kotlin’s DSL capabilities make it easy to define grammars for different domains, enabling developers to build powerful abstractions across use cases.

The Key Feature: Lambdas with Receivers

The magic behind Kotlin DSLs lies in lambdas with receivers.

In the HTML DSL, when you call table { ... }, the table element acts as the receiver. This allows nested blocks like tr { ... } and td { ... } to access its scope directly, creating a natural, hierarchical structure that mirrors HTML itself.

This feature makes DSLs:

  • Readable — code mirrors the structure of the output
  • Maintainable — changes are easy to apply across structures
  • Error-resistant — misuse of tags or nesting gets caught at compile-time

Conclusion

Using internal DSLs in Kotlin — like kotlinx.html—isn’t just about writing code that looks like HTML. It’s about writing safer, more maintainable, and dynamic code that can scale with your project.

Whether you’re generating HTML, XML, or custom structured data, DSLs provide a powerful tool in a Kotlin developer’s toolkit. By leveraging lambdas with receivers and the expressive power of Kotlin, you can create elegant solutions tailored to your domain.

FAQs

Q: What is an internal DSL in Kotlin?
An internal DSL is a domain-specific language built within Kotlin using its existing syntax and features — like lambdas with receivers — to create readable, specialized code for a specific purpose such as HTML generation.

Q: Why prefer Kotlin DSL over plain HTML text?
Kotlin DSLs provide compile-time safety, reduce markup errors, and allow you to use Kotlin’s control structures, making the HTML generation dynamic and robust.

Q: Can this approach be used for XML or other markup languages?
Yes, the same DSL principles apply to XML or similar hierarchical languages, making it easy to adapt the code for various structured content production.

Q: What are lambdas with receivers?
They are functions that have an implicit receiver object, allowing direct access to its members within the lambda, enabling clean DSL-like syntax.

Java-Kotlin Interoperability

Java-Kotlin Interoperability (Vice Versa): A Comprehensive Guide

Java and Kotlin are both official languages for Android development, and one of Kotlin’s biggest strengths is its seamless interoperability with Java. This allows developers to migrate projects gradually, use existing Java libraries, and leverage Kotlin’s modern features without abandoning Java entirely.

In this blog, we will explore how Kotlin interacts with Java, focusing on:

  • Calling Java code from Kotlin
  • Calling Kotlin code from Java
  • Handling nullability
  • Working with Java collections
  • Overcoming common interoperability challenges

By the end of this guide, you’ll have a solid understanding of Java-Kotlin interoperability and how to make the most of both languages in a single project.

Why Java Interoperability Matters in Kotlin?

Since Java has been around for decades, a vast number of libraries, frameworks, and applications are built with it. Kotlin’s interoperability ensures that:

  • You can migrate to Kotlin incrementally instead of rewriting entire projects.
  • Existing Java libraries (e.g., Retrofit, Glide) can be used in Kotlin without modification.
  • Teams can work with both languages in the same project.

Calling Java Code from Kotlin

Using Java classes in Kotlin is straightforward. Kotlin treats Java code almost as if it were native.

Java
// Java class
public class User {
    private String name;
    public User(String name) { this.name = name; }
    public String getName() { return name; }
}
Kotlin
// Kotlin usage
val user = User("amol")
println(user.name) // Calls getName() seamlessly

Kotlin automatically maps Java getters and setters to properties, making the syntax cleaner.

Calling Kotlin Code from Java

The reverse is also possible: Java can call Kotlin code. However, some Kotlin features don’t translate directly, so annotations help.

Kotlin
class Utils {
    @JvmStatic
    fun printMessage(msg: String) {
        println(msg)
    }
}
Java
// Java usage
Utils.printMessage("Hello from Java");

Here, @JvmStatic ensures the Kotlin function behaves like a regular Java static method.

Handling Nullability

One of Kotlin’s core advantages is null safety. When calling Java code, Kotlin treats platform types cautiously:

  • A Java type like String might be nullable or non-nullable, and Kotlin lets you decide how to handle it.
  • Use Kotlin’s safe call (?.) and elvis operator (?:) to protect against NullPointerException.
Kotlin
val length = javaUser.name?.length ?: 0

This guarantees safety when working with Java APIs that may return null.

Working with Java Collections

Kotlin distinguishes between mutable and immutable collections, while Java does not.

  • A List<String> in Kotlin may map to a List<String> in Java but can cause confusion if mutability expectations differ.
  • To avoid issues, be explicit when converting collections between Kotlin and Java using methods like toList() or toMutableList().

Common Interoperability Challenges and Solutions

  • Default parameters in Kotlin — Java doesn’t support them. Use @JvmOverloads to generate overloaded versions.
  • Companion objects — add @JvmStatic for Java-friendly access.
  • Checked exceptions — Java requires them, Kotlin doesn’t. When calling Java code, handle exceptions properly.

By following these practices, you minimize friction between the two languages.

Conclusion

Kotlin’s interoperability with Java is one of its biggest advantages, allowing developers to:

  • Gradually migrate projects
  • Use existing Java libraries
  • Leverage modern Kotlin features alongside Java

Understanding how to handle null safety, collections, and special Kotlin features in Java ensures smooth integration between the two languages. By following best practices and using annotations like @JvmOverloads and @JvmStatic, you can build efficient, maintainable, and error-free applications.

If you’re transitioning from Java to Kotlin, start small by calling Java code from Kotlin before diving deeper into full migration.

Happy migrating..!

Symbolic AI Explained

Symbolic AI Explained Simply: How It Thinks Like Humans

Artificial Intelligence (AI) comes in many flavors, but one of the oldest and most fascinating approaches is Symbolic AI. Unlike modern machine learning models that crunch massive datasets to “learn patterns,” symbolic AI tries to mimic how humans reason and solve problems using logic, symbols, and rules.

In this blog, we’ll break down symbolic AI in simple terms, show you how it “thinks,” and even walk through some real life examples.

What Is Symbolic AI?

Symbolic AI is a branch of AI that represents knowledge using symbols (like words or numbers) and manipulates them with rules (logic statements).

Think of it this way:

  • Humans use language, concepts, and reasoning to solve problems.
  • Symbolic AI does the same but in a structured way, using rules like if-then statements.

For example:

  • If it’s raining, then take an umbrella.
  • If you’re hungry, then eat food.

This logical reasoning is exactly what symbolic AI systems are built to do.

Why It’s Like Human Thinking

Our brains often work by categorizing and reasoning. If you know that “all birds can fly” and “a sparrow is a bird,” you can infer that “a sparrow can fly.”

Symbolic AI follows the same process:

  1. Store facts (sparrow is a bird)
  2. Store rules (all birds can fly).
  3. Apply logic (therefore, sparrow can fly).

This makes it interpretable and transparent — unlike black-box neural networks where decisions are often hidden inside layers of weights and biases.

Real-World Applications of Symbolic AI

Even though deep learning dominates headlines today, symbolic AI still powers many systems you use daily:

  • Expert systems in medicine that suggest diagnoses.
  • Search engines that use symbolic reasoning for understanding relationships between words.
  • Chatbots that rely on logic-based conversation flows.
  • Knowledge graphs (like Google’s Knowledge Panel) to connect concepts.

Symbolic Reasoning in Python

Let’s see how symbolic AI works with a small example using the experta library, which is designed for rule-based systems in Python.

Install Experta

Python
pip install experta

Example Code: Animal Classification

Kotlin
from experta import *

class AnimalFacts(KnowledgeEngine):

    @Rule(Fact(has_feathers=True), Fact(can_fly=True))
    def bird(self):
        print("This is likely a Bird.")

    @Rule(Fact(has_fur=True), Fact(says="meow"))
    def cat(self):
        print("This is likely a Cat.")

    @Rule(Fact(has_fur=True), Fact(says="woof"))
    def dog(self):
        print("This is likely a Dog.")

# Run the engine
engine = AnimalFacts()
engine.reset()

# Insert facts
engine.declare(Fact(has_fur=True))
engine.declare(Fact(says="woof"))

engine.run()

Define rules — Each @Rule tells the system how to reason with facts.

  • If something has feathers and can fly → it’s a bird.
  • If something has fur and says “meow” → it’s a cat.
  • If something has fur and says “woof” → it’s a dog.

Declare facts — You feed the system with facts (like “has_fur=True”).

Run the engine — The rules are applied, and the AI makes an inference.

When we run this example, the system prints:

Python
This is likely a Dog.

That’s symbolic AI at work — reasoning step by step like a human would. 

Strengths and Weaknesses of Symbolic AI

Strengths:

  • Easy to explain (transparent reasoning).
  • Good for domains where rules are clear (like medical diagnosis or legal reasoning).
  • Works well with structured knowledge (knowledge graphs, ontologies).

Weaknesses:

  • Struggles with ambiguity or incomplete data.
  • Hard to scale for real-world complexity (imagine writing rules for every possible situation).
  • Less effective for tasks like image recognition, where patterns matter more than explicit rules.

Symbolic AI vs Machine Learning

  • Symbolic AI = Thinks like a human using rules and logic.
  • Machine Learning = Learns patterns from data, often without explicit rules.

The future of AI is likely a hybrid of both:

  • Symbolic AI for reasoning.
  • Machine learning for perception (like vision and speech).

This combination is sometimes called Neuro-Symbolic AI, a promising direction that merges the best of both worlds.

Conclusion

Symbolic AI may not be as flashy as deep learning, but it’s one of the most human-like approaches to building intelligent systems. It reasons, explains, and draws logical conclusions in a way we can understand.

As AI evolves, expect to see symbolic methods come back stronger — especially in areas where transparency, logic, and human-like reasoning matter most.

Controlling the Java API with Kotlin Annotations

Controlling the Java API with Kotlin Annotations: A Practical Guide

In today’s multi-language development environments, seamless interoperability between Kotlin and Java is essential. Kotlin offers a powerful set of annotations specifically designed to control how Kotlin code compiles into Java bytecode, optimizing the way Java callers interact with Kotlin declarations. These annotations help bridge the language gap by addressing naming conventions, method accessibility, and property exposure, ensuring smooth integration with existing Java codebases.

Why Kotlin Annotations Matter for Java Interoperability

Kotlin’s interoperability with Java allows developers to gradually introduce Kotlin into Java projects or use libraries across both languages effortlessly. However, due to differences in language features and conventions, some Kotlin declarations don’t naturally translate into idiomatic or expected Java APIs. That’s where Kotlin’s annotations come in: they fine-tune the compiled bytecode so that Java code can consume Kotlin components as naturally as if they were written in Java.

Key Kotlin Annotations That Control Java API Exposure

1. @Volatile and @Strictfp: Replacing Java Keywords

  • @Volatile: Acts as a direct replacement for Java’s volatile keyword. Applying @Volatile to a Kotlin property ensures that it will behave as a volatile field in the Java bytecode, maintaining consistency in visibility and ordering guarantees in multi-threaded contexts.
  • @Strictfp: Corresponds to Java’s strictfp keyword ensuring that floating-point calculations adhere to IEEE 754 standards. Use this annotation on methods or classes when precision and determinism of floating-point operations are critical in Java interoperability.

2. @JvmName: Customize Method and Field Names

Kotlin’s default naming conventions may differ from what Java code expects. The @JvmName annotation lets you explicitly rename methods or fields generated from Kotlin declarations when called from Java. This is especially useful when:

  • Migrating or interoperating with legacy Java code needing specific method signatures.
  • Avoiding name clashes or improving readability in Java consumers.
Kotlin
@JvmName("customMethodName")<br>fun originalKotlinFunction() { ... }

3. @JvmStatic: Expose Static Methods for Java

In Kotlin, functions inside object declarations or companion objects are not static by default. Applying @JvmStatic to these methods exposes them as true static methods in Java, allowing calls without needing an instance reference. This boosts performance and aligns with Java’s static access patterns.

4. @JvmOverloads: Generate Overloaded Methods for Default Parameters

Kotlin’s support for default parameter values is elegant but not directly accessible in Java because Java lacks this feature. Using @JvmOverloads tells the compiler to create multiple overloaded versions of a function, each omitting one or more parameters with defaults. This simplifies calling Kotlin functions from Java, offering multiple method signatures that cover all default value scenarios.

Kotlin
@JvmOverloads
fun greet(name: String = "Guest", age: Int = 18) { ... }

Java callers get:

  • greet()
  • greet(String name)
  • greet(String name, int age)

5. @JvmField: Direct Field Exposure Without Accessors

By default, Kotlin generates private fields with public getters and setters for properties. Applying @JvmField bypasses these accessors and exposes the property as a public Java field directly. This is useful when:

  • You need maximum performance with direct field access in Java.
  • Working with frameworks or libraries expecting public fields rather than methods.

Enhancing Kotlin-Java Integration With Annotations

These annotations collectively give developers fine-grained control over Kotlin-to-Java compilation output, tailoring APIs for:

  • Compatibility: Ensuring Kotlin code fits naturally into Java’s language and runtime paradigms.
  • Flexibility: Allowing developers to customize method names, parameters, and access patterns.
  • Performance: Cutting down synthetic method calls where direct field access or static calls are preferable.
  • Maintainability: Smoothly integrating Kotlin into existing Java codebases without awkward wrappers.

Conclusion

Understanding and leveraging Kotlin’s annotations like @JvmName, @JvmStatic, @JvmOverloads, @Volatile, @Strictfp, and @JvmField unlocks powerful control over how Kotlin code appears and behaves from Java. This knowledge is essential for teams working in mixed-language environments to ensure seamless, efficient, and maintainable integration. By applying these best practices, developers can confidently bridge Kotlin and Java, blending modern syntax with established Java conventions.

Frequently Asked Questions (FAQ)

Q1: Why use @JvmStatic instead of regular Kotlin object methods?
A1: @JvmStatic exposes the method as a Java static method, allowing calls without needing an instance. This matches Java’s typical usage for static utility functions and improves performance.

Q2: Can I rename Kotlin methods for Java callers?
A2: Yes, the @JvmName annotation lets you change method or field names as seen by Java, helping with compatibility or clearer APIs.

Q3: How does @JvmOverloads help with default parameters?
A3: It generates overloaded Java methods corresponding to Kotlin defaults, making it easier to call those functions from Java without manually specifying every argument.

Q4: When should I use @JvmField?
A4: Use @JvmField when you want to expose Kotlin properties as public Java fields directly, avoiding the creation of getter/setter methods for use-cases requiring direct field access.

Q5: What is the difference between @Volatile and @Strictfp?
A5: @Volatile marks a Kotlin property to behave like a volatile Java field for thread safety. @Strictfp enforces strict floating-point behavior compatible with Java’s strictfp keyword.

Structure of Domain-Specific Languages (DSLs) in Kotlin

Understanding the Structure of Domain-Specific Languages (DSLs) in Kotlin

When developing software, developers often rely on APIs to interact with libraries and frameworks. However, in many cases, a Domain-Specific Language (DSL) can provide a more natural, readable, and concise way to express intent. Unlike regular APIs, DSLs offer a structured grammar, making them feel more like a “mini-language” tailored to a particular problem domain.

In this article, we’ll explore the structure of DSLs, how they differ from command-query APIs, and why DSLs are powerful tools in software design. We’ll also look at practical Kotlin examples that showcase how DSLs improve readability, maintainability, and developer experience.

What Makes a DSL Different from a Regular API?

The distinction between a DSL and a regular Application Programming Interface (API) is not always clear-cut. While APIs expose methods and expect developers to chain or sequence them manually, DSLs introduce a formal structure (grammar) that governs how operations are expressed.

  • APIs → Command-query style, no inherent structure.
  • DSLs → Structured grammar, readable syntax, and context-aware function chaining.

This structure makes DSLs comparable to natural languages. Just as grammar allows English sentences to be understood, DSL grammar ensures that function calls and operators form meaningful expressions.

How Structure Emerges in DSLs

Nesting and Lambdas in Kotlin

Kotlin DSLs often rely on nested lambdas or chained method calls to express complex operations.
 Example:

Kotlin
dependencies {
    compile("junit:junit:4.11")
    compile("com.google.inject:guice:4.1.0")
}

Here, the structure allows you to declare dependencies without repeatedly calling project.dependencies.add(...), making the code concise and context-aware.

Chained Method Calls in Test Frameworks

DSLs also shine in testing frameworks. Consider Kotlintest:

Kotlin
str should startWith("kot")

This is cleaner and more expressive than the JUnit equivalent:

Kotlin
assertTrue(str.startsWith("kot"))

By structuring assertions as chained calls, DSLs make the intent of the code immediately clear.

Context Reuse

A major advantage of DSLs is their ability to reuse context across multiple function calls. This avoids repetition, reduces boilerplate, and improves readability.

For example:

  • DSLs → Single compile context for multiple dependencies.
  • APIs → Must repeat "compile" keyword in every call.

Why DSLs Matter for Developers

DSLs are more than syntactic sugar — they fundamentally improve how developers interact with frameworks:

  • Readability → Code looks closer to natural language.
  • Maintainability → Less boilerplate, fewer errors.
  • Expressiveness → Clear mapping between domain concepts and code.
  • Productivity → Faster development and reduced cognitive load.

This explains why DSLs are popular in tools like Gradle build scripts, Kotlin test frameworks, and SQL-like query builders.

Conclusion

Domain-Specific Languages (DSLs) bridge the gap between programming and human-readable domain logic. By introducing grammar, structure, and context-awareness, DSLs make code cleaner, more expressive, and easier to maintain compared to traditional APIs.

For developers, embracing DSLs in the right context can lead to:

  • Faster onboarding for new team members
  • More concise build and test scripts
  • Reduced boilerplate in complex projects

As the Kotlin ecosystem and modern frameworks evolve, DSLs will continue to play a central role in improving developer productivity and code clarity.

FAQs

Q1: What is the main difference between a DSL and an API?
 A DSL introduces grammar and structure, while APIs rely on sequential commands without inherent structure.

Q2: Why are Kotlin DSLs popular in Gradle scripts?
 They allow developers to express dependencies concisely, avoiding repetitive boilerplate.

Q3: Can DSLs replace APIs completely?
 No — DSLs are built on top of APIs. They complement APIs by making interactions more expressive.

Shared ViewModels in Android

Understanding Shared ViewModels in Android: A Comprehensive Guide

In modern Android development, ViewModel has become an indispensable component for managing UI-related data in a lifecycle-conscious manner. One powerful application of ViewModels is sharing data between multiple fragments or activities. This guide provides a deep dive into shared ViewModels, explaining their purpose, implementation, and best practices for creating seamless data sharing in your Android apps.

The Concept of Shared ViewModels

A Shared ViewModel is a ViewModel instance that is accessible across multiple fragments or activities, enabling shared state management. This approach is ideal when:

  • Fragment Communication: Multiple fragments need to work with the same data, such as a user profile or settings.
  • Decoupling Logic: You want fragments to exchange information without creating brittle, tightly-coupled dependencies.
  • Navigation Component Scenarios: Sharing data across destinations within a navigation graph requires clean state management.

Unlike standalone ViewModels scoped to a single UI component, shared ViewModels can be scoped to an entire activity or a specific navigation graph, allowing seamless state sharing while respecting lifecycle boundaries.

Why Use Shared ViewModels?

Here are some compelling reasons to choose shared ViewModels:

  1. Lifecycle Safety: Data stored in a ViewModel persists through configuration changes like screen rotations, avoiding unwanted resets.
  2. Simplified Communication: Fragments don’t need to interact directly, reducing the risk of complex dependencies and bugs.
  3. Consistent Data: A single source of truth ensures data integrity and synchronization across multiple components.
  4. Modern Architecture: Shared ViewModels align perfectly with MVVM (Model-View-ViewModel) architecture, a best practice for building scalable Android apps.

Step-by-Step Implementation of Shared ViewModels

Setting Up Dependencies

Add the core libraries you’ll need (use the latest stable versions from AndroidX/Hilt):

Kotlin
// app/build.gradle.kts
dependencies {
    // Fragments & Activity KTX
    implementation("androidx.fragment:fragment-ktx:<ver>")
    implementation("androidx.activity:activity-ktx:<ver>")

    // Lifecycle / ViewModel / coroutines support
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:<ver>")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:<ver>")

    // Only if you still use LiveData:
    implementation("androidx.lifecycle:lifecycle-livedata-ktx:<ver>")

    // Jetpack Navigation (Fragment)
    implementation("androidx.navigation:navigation-fragment-ktx:<ver>")
    implementation("androidx.navigation:navigation-ui-ktx:<ver>")

    // (Optional) Hilt for DI + ViewModels
    implementation("com.google.dagger:hilt-android:<ver>")
    kapt("com.google.dagger:hilt-android-compiler:<ver>")
    implementation("androidx.hilt:hilt-navigation-fragment:<ver>")
    kapt("androidx.hilt:hilt-compiler:<ver>")
}

If using Hilt, also apply the plugin in your module’s Gradle file:

Kotlin
plugins {
    id("com.google.dagger.hilt.android")
    kotlin("kapt")
}

Designing the Shared ViewModel

Prefer a single source of UI state with immutable data classes and expose it via StateFlow. Keep side effects (like toasts or navigation) separate using a SharedFlow for one-off events.

Kotlin
// Shared ViewModel example (Kotlin)
@HiltViewModel // Remove if you’re not using Hilt
class ProfileSharedViewModel @Inject constructor(
    private val repo: ProfileRepository,            // Your data source
    private val savedStateHandle: SavedStateHandle  // For process death & args
) : ViewModel() {

    data class UiState(
        val user: User? = null,
        val isLoading: Boolean = false,
        val error: String? = null
    )

    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState

    // One-off events (navigation, snackbar, etc.)
    private val _events = MutableSharedFlow<Event>()
    val events: SharedFlow<Event> = _events

    sealed interface Event { object Saved : Event }

    fun load(userId: String) {
        // Example of persisting inputs using SavedStateHandle
        savedStateHandle["lastUserId"] = userId

        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true, error = null) }
            runCatching { repo.fetchUser(userId) }
                .onSuccess { user -> _uiState.update { it.copy(user = user, isLoading = false) } }
                .onFailure { e -> _uiState.update { it.copy(isLoading = false, error = e.message) } }
        }
    }

    fun updateName(newName: String) {
        _uiState.update { state ->
            state.copy(user = state.user?.copy(name = newName))
        }
    }

    fun save() {
        val current = _uiState.value.user ?: return
        viewModelScope.launch {
            runCatching { repo.saveUser(current) }
                .onSuccess { _events.emit(Event.Saved) }
                .onFailure { e -> _uiState.update { it.copy(error = e.message) } }
        }
    }
}

SavedStateHandle survives process death when used with Navigation and lets you read nav arguments via savedStateHandle.get<T>("arg") or create StateFlows: savedStateHandle.getStateFlow("key", default).

Scoping Options (Activity vs. Nav Graph)

Activity scope — share across all fragments in the same activity:

Kotlin
private val vm: ProfileSharedViewModel by activityViewModels()
  • Lives as long as the Activity is alive (across configuration changes).
  • Good for app-wide state within that activity (e.g., cart, session, toolbar state).

Navigation graph scope — share only within a specific flow:

Kotlin
private val vm: ProfileSharedViewModel by navGraphViewModels(R.id.profile_graph)
  • One instance per NavBackStackEntry for that graph.
  • Cleared when that graph is popped off the back stack.
  • Great for multi-step wizards (e.g., signup → verify → done).

Using Hilt? Get a Hilt-injected, nav-graph–scoped VM with:

Kotlin
private val vm: ProfileSharedViewModel by hiltNavGraphViewModels(R.id.profile_graph)

Avoid sharing a ViewModel across different activities. Use a repository/single source of truth instead, or adopt a single-activity architecture.

Using the Shared ViewModel in Fragments

Collect state with lifecycle awareness. Use repeatOnLifecycle so collection stops when the view is not visible.

Kotlin
@AndroidEntryPoint // if using Hilt
class EditProfileFragment : Fragment(R.layout.fragment_edit_profile) {

    private val vm: ProfileSharedViewModel by activityViewModels() // or navGraphViewModels(...)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // State
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                vm.uiState.collect { state ->
                    // update text fields, progress bars, errors
                }
            }
        }

        // One-off events
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                vm.events.collect { event ->
                    when (event) {
                        is ProfileSharedViewModel.Event.Saved -> {
                            // e.g., findNavController().navigateUp()
                        }
                    }
                }
            }
        }

        // Example inputs
        val save = view.findViewById<Button>(R.id.saveButton)
        save.setOnClickListener { vm.save() }
    }
}

And another fragment in the same scope sees the same instance:

Kotlin
class PreviewProfileFragment : Fragment(R.layout.fragment_preview_profile) {
    private val vm: ProfileSharedViewModel by activityViewModels()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                vm.uiState.collect { state ->
                    // render preview using state.user
                }
            }
        }
    }
}

If you prefer LiveData:

Kotlin
vm.liveData.observe(viewLifecycleOwner) { state -> /* ... */ }

Passing Arguments & Using SavedStateHandle

When you navigate with arguments, Navigation stores them in the destination’s SavedStateHandle, which the ViewModel can read:

Kotlin
// In the ViewModel (constructor already has savedStateHandle)
private val userId: String? = savedStateHandle["userId"]

init {
    userId?.let(::load)
}

You can also write to the handle to restore after process death:

Kotlin
savedStateHandle["draftName"] = "Amol"
val draftNameFlow = savedStateHandle.getStateFlow("draftName", "")

Handling One-Off Events Correctly

Never mix events with state (or they re-trigger on rotation). Use SharedFlow (or Channel) for fire-and-forget actions:

Kotlin
private val _events = MutableSharedFlow<Event>(extraBufferCapacity = 1)
val events = _events.asSharedFlow()

// Emit: _events.tryEmit(Event.Saved)

Testing a Shared ViewModel

Use the coroutine test utilities and a fake repository:

Kotlin
@OptIn(ExperimentalCoroutinesApi::class)
class ProfileSharedViewModelTest {

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule() // sets Dispatchers.Main to a TestDispatcher

    private val repo = FakeProfileRepository()
    private lateinit var vm: ProfileSharedViewModel

    @Before 
    fun setUp() {
        vm = ProfileSharedViewModel(repo, SavedStateHandle())
    }

    @Test 
    fun `load populates user and clears loading`() = runTest {
        vm.load("42")
        val state = vm.uiState.first { !it.isLoading }
        assertEquals("42", state.user?.id)
        assertNull(state.error)
    }
}

Implement MainDispatcherRule by swapping Dispatchers.Main with a StandardTestDispatcher. Keep repositories pure and synchronous in tests, or use runTest with fakes.

When to Choose Each Scope

Use Activity scope when:

  • Tabs/bottom navigation fragments need the same state.
  • Data lives for the whole activity session (e.g., cart, auth session).

Use Nav-graph scope when:

  • Data is local to a flow (onboarding, multi-step form).
  • You want the ViewModel cleared when the flow finishes (pop).

Best Practices

  • Expose immutable state (StateFlow, LiveData) and keep mutables private.
  • Don’t hold views/context inside ViewModels. Inject repositories/use cases instead.
  • Use viewLifecycleOwner when observing in fragments (not this), to avoid leaks.
  • Keep UI state small & serializable if you rely on SavedStateHandle.
  • Model errors in state and display them; don’t throw them up to the UI.
  • Avoid shared ViewModels across activities; share via repository or a data layer.
  • Prefer StateFlow for new code; LiveData is still fine if your app already uses it.

Common Pitfalls (and Fixes)

  • State replays on rotation (toast fires again): Use SharedFlow/Channel for events, not StateFlow/LiveData.
  • ViewModel not shared between fragments: Ensure both fragments use the same scope (activityViewModels() or the same navGraphId).
  • ViewModel survives too long: You probably used activity scope where a nav-graph scope made more sense.
  • Collectors keep running off-screen: Wrap collect in repeatOnLifecycle(Lifecycle.State.STARTED).

Minimal, End-to-End Example

Navigation graph (excerpt):

XML
<!-- res/navigation/profile_graph.xml -->
<navigation
    android:id="@+id/profile_graph"
    app:startDestination="@id/editProfileFragment">

    <fragment
        android:id="@+id/editProfileFragment"
        android:name="com.example.EditProfileFragment">
        <action
            android:id="@+id/action_edit_to_preview"
            app:destination="@id/previewProfileFragment" />
        <argument
            android:name="userId"
            app:argType="string" />
    </fragment>

    <fragment
        android:id="@+id/previewProfileFragment"
        android:name="com.example.PreviewProfileFragment" />
</navigation>

Fragments sharing the same ViewModel via nav graph:

Kotlin
class EditProfileFragment : Fragment(R.layout.fragment_edit_profile) {
    private val vm: ProfileSharedViewModel by navGraphViewModels(R.id.profile_graph)
    // collect uiState/events as shown earlier…
}

class PreviewProfileFragment : Fragment(R.layout.fragment_preview_profile) {
    private val vm: ProfileSharedViewModel by navGraphViewModels(R.id.profile_graph)
    // collect uiState and render preview…
}

Conclusion

Shared ViewModels let fragments share state safely without talking to each other directly. Scope them to the activity for app-wide state or to a navigation graph for flow-scoped state. Expose state with StateFlow, drive UI with lifecycle-aware collectors, use SavedStateHandle for resilience, and keep one-off events separate. Follow these patterns and you’ll get predictable, testable, and decoupled UI flows.

State Management in Jetpack Compose

Mastering State Management in Jetpack Compose: A Comprehensive Guide

State management is one of the most critical aspects of building dynamic and interactive Android applications. With Jetpack Compose, Android’s modern UI toolkit, managing state becomes more intuitive, but it also introduces new paradigms that developers need to understand. In this blog, we’ll explore state management in Jetpack Compose in detail. We’ll break down essential...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Digital Signature

What Is a Digital Signature & How SSL Certificates Work on Android Devices

In today’s digital world, security and trust are essential, especially when it comes to sensitive information exchanged over the internet. Two foundational technologies that play a critical role in ensuring online security are digital signatures and SSL certificates. If you’re an Android user or developer, understanding these concepts is crucial for protecting your data and securing communications. 

This blog will explain what a digital signature is, how SSL certificates work on Android devices, and their importance.

What Is a Digital Signature?

A digital signature is a kind of electronic fingerprint — a unique code attached to digital documents or messages that proves their authenticity and integrity. Think of it like a handwritten signature but much more secure because it’s based on cryptography.

Why Are Digital Signatures Important?

  • Authentication: Verifies the sender’s identity.
  • Integrity: Ensures the message or document has not been altered after signing.
  • Non-repudiation: The sender cannot deny having sent the message.

Digital signatures use a pair of keys: a private key (known only to the signer) and a public key (shared with others). When you sign a document, your device uses your private key to create a unique signature. Others can use your public key to verify that signature’s authenticity.

How Digital Signatures Work

Let’s look at a simplified workflow using cryptographic functions in Android’s Java/Kotlin environment to understand the digital signature process.

Kotlin
// Generating a digital signature in Android using Java

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;

public class DigitalSignatureExample {

    public static void main(String[] args) throws Exception {

        // Step 1: Generate key pair (public and private keys)
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
        keyGen.initialize(2048);
        KeyPair pair = keyGen.generateKeyPair();
        PrivateKey privateKey = pair.getPrivate();
        PublicKey publicKey = pair.getPublic();

        // Step 2: Sign data
        String data = "This is a message to sign";
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(privateKey);
        signature.update(data.getBytes());
        byte[] digitalSignature = signature.sign();

        // Step 3: Verify signature
        Signature verifier = Signature.getInstance("SHA256withRSA");
        verifier.initVerify(publicKey);
        verifier.update(data.getBytes());
        boolean isVerified = verifier.verify(digitalSignature);
        System.out.println("Signature Verified: " + isVerified);
    }
}

Here,

  • Step 1: We create a key pair using RSA, a popular cryptographic algorithm.
  • Step 2: Using the private key, we “sign” the data. The process hashes the data and encrypts it with the private key to create the digital signature.
  • Step 3: Anyone with the matching public key can verify the signature. They hash the original data and decrypt the signature to confirm both match, ensuring the data is authentic and untampered.

What Are SSL Certificates?

An SSL (Secure Sockets Layer) certificate is a digital certificate that authenticates a website’s identity and enables an encrypted connection. When you visit a website with HTTPS, the SSL certificate is what makes the communication between your device (like an Android phone) and the website secure.

Key Features of SSL Certificates

  • Encryption: They encrypt data sent between your browser and the web server.
  • Authentication: They confirm the website’s identity using a digital signature issued by a trusted Certificate Authority (CA).
  • Data Integrity: They ensure data is not altered during transmission.

How SSL Certificates Work on Android Devices

When your Android device connects to an HTTPS website, a process called the SSL/TLS handshake happens. This is a behind-the-scenes conversation between your device and the web server to establish a secure encrypted connection.

The SSL/TLS Handshake Steps Simplified

1. Client Hello: Your Android device sends a request to the server saying it wants to connect securely, including which encryption methods it supports.

2. Server Hello & Certificate: The server responds with its SSL certificate, which contains its public key and the digital signature from a CA to prove authenticity.

3. Verification: Your Android device verifies the certificate by checking:

  • Is the certificate issued by a trusted CA (Android maintains a list of trusted root certificates)?
  • Is the certificate valid and not expired or revoked?
  • Does the domain match the certificate?

4. Session Key Creation: Once verified, your device and the server create a shared secret key for encrypting data during the session.

5. Secure Communication: All data transferred is encrypted with this session key, keeping your information safe from eavesdroppers.

Why Are Digital Signatures Integral to SSL Certificates?

The digital signature within an SSL certificate is created by a trusted Certificate Authority (CA). This signature vouches for the authenticity of the certificate, confirming the server’s identity. Without this digital signature, an SSL certificate wouldn’t be trustworthy, and your Android device couldn’t be sure it’s communicating with the intended server.

Why You Should Care About Digital Signatures & SSL on Android

  • Digital signatures are essential for verifying identity and data integrity.
  • SSL certificates use digital signatures to secure websites.
  • Android devices use SSL certificates to ensure safe browsing and protect user data.
  • Developers should understand how to implement and verify digital signatures to build secure Android apps.

By grasping these concepts, you empower yourself to better protect your digital life, whether you’re surfing the web or developing mobile apps.

error: Content is protected !!