<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sridhar Subramani</title>
    <description>The latest articles on DEV Community by Sridhar Subramani (@sridharsubramani).</description>
    <link>https://dev.to/sridharsubramani</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F710121%2Fe571aa1b-28b9-4685-a023-e85bf8b441b2.jpeg</url>
      <title>DEV Community: Sridhar Subramani</title>
      <link>https://dev.to/sridharsubramani</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sridharsubramani"/>
    <language>en</language>
    <item>
      <title>AI Needs More Than Just a Model: The Missing Role of Context, Orchestration, and Agents</title>
      <dc:creator>Sridhar Subramani</dc:creator>
      <pubDate>Sun, 23 Mar 2025 16:40:26 +0000</pubDate>
      <link>https://dev.to/sridharsubramani/ai-needs-more-than-just-a-model-the-missing-role-of-context-orchestration-and-agents-hgh</link>
      <guid>https://dev.to/sridharsubramani/ai-needs-more-than-just-a-model-the-missing-role-of-context-orchestration-and-agents-hgh</guid>
      <description>&lt;h4&gt;
  
  
  Solving the AI Puzzle: Why External Context, Orchestration, and Agents Are Key to Intelligence
&lt;/h4&gt;




&lt;h3&gt;
  
  
  Disclaimer:
&lt;/h3&gt;

&lt;p&gt;This blog is intended as a beginner-friendly guide to AI, specifically focusing on why Large Language Models (LLMs) should interact with agents to solve any AI query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It does not&lt;/strong&gt; cover advanced topics such as &lt;strong&gt;training&lt;/strong&gt;, &lt;strong&gt;fine-tuning&lt;/strong&gt;, or &lt;strong&gt;building LLMs&lt;/strong&gt; from scratch because, at the time of writing, I am not deeply familiar with these areas. However, my experience comes from building an &lt;strong&gt;in-house orchestration system&lt;/strong&gt;, which serves as the central component for facilitating communication between different AI components to solve any AI query. This system routes requests from input agents to classifiers and further to specific agents and tools, ultimately generating a response.&lt;/p&gt;

&lt;p&gt;The goal is to introduce &lt;strong&gt;key concepts&lt;/strong&gt;, &lt;strong&gt;terminology&lt;/strong&gt;, and &lt;strong&gt;importance of Agents&lt;/strong&gt; and the need for Orchestration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target Audience:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Beginners in AI&lt;/strong&gt; who are exploring concepts like LLMs, agents, and orchestration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developers from Non-AI Domain &amp;amp; Enthusiasts&lt;/strong&gt; looking to understand how AI models interact with real-world systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product Managers&lt;/strong&gt; seeking high-level insights without deep technical details.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Analogy
&lt;/h2&gt;

&lt;p&gt;If we think of the &lt;strong&gt;human brain&lt;/strong&gt; as an &lt;strong&gt;AI model&lt;/strong&gt; (e.g., an LLM), then the hands, vocal cords, eyes, etc. would be its &lt;strong&gt;agents&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In humans, the brain processes sensory inputs and issues commands to physical agents (e.g., muscles). Here's a simplified analogy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your &lt;strong&gt;retina captures visual data&lt;/strong&gt; of a ball coming toward you. The &lt;strong&gt;retina&lt;/strong&gt; acts as an &lt;strong&gt;input agent&lt;/strong&gt;, sending this data to the brain for processing.&lt;/li&gt;
&lt;li&gt;Your brain analyzes the visual input and decides that you need to catch the ball. The brain functions as the &lt;strong&gt;LLM&lt;/strong&gt; in this analogy.&lt;/li&gt;
&lt;li&gt;The brain sends a signal to your hands to catch the ball. Your &lt;strong&gt;hands serve&lt;/strong&gt; as the &lt;strong&gt;output agent&lt;/strong&gt;, executing the action.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the brain (LLM) is trained properly, it instructs your hands to act correctly. However, if the training is insufficient, it might make an incorrect decision, such as instructing your eyes to close instead of catching the ball, leaving you vulnerable to an accident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI (LLM) can process input, reason with it, and generate outputs,&lt;/strong&gt; much like the &lt;strong&gt;brain processes sensory data and makes decisions.&lt;/strong&gt; However, just as the &lt;strong&gt;brain requires physical agents&lt;/strong&gt; like hands to interact with the environment, &lt;strong&gt;LLMs require agents&lt;/strong&gt; to bridge the &lt;strong&gt;gap between reasoning and action.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Assume we use &lt;strong&gt;LLaMA 3.2&lt;/strong&gt; as the &lt;strong&gt;LLM&lt;/strong&gt;. Our primary &lt;strong&gt;input&lt;/strong&gt; and &lt;strong&gt;output&lt;/strong&gt; type is &lt;strong&gt;text&lt;/strong&gt;, which makes interaction straightforward. While this works well for text-based outputs like answering questions, it doesn't suffice for all use cases. For instance, text output is suitable when we only want an answer, but if we need to perform an action (e.g., sending an email, opening a file), the LLM cannot handle this because its sole responsibility is to generate text-based outputs. It has no inherent capability to perform actions or interact with the physical or digital world.&lt;/p&gt;




&lt;h2&gt;
  
  
  Importance of Agents in AI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What are Agents
&lt;/h3&gt;

&lt;p&gt;From our earlier analogy, we assumed that our hands and eyes are agents in the physical world. In the digital world, we call the &lt;strong&gt;software component an agent;&lt;/strong&gt; an agent is anything that can &lt;strong&gt;enhance&lt;/strong&gt; your &lt;strong&gt;LLM&lt;/strong&gt; to &lt;strong&gt;read&lt;/strong&gt; its &lt;strong&gt;environment&lt;/strong&gt;, and it can &lt;strong&gt;provide&lt;/strong&gt; some &lt;strong&gt;actionable output&lt;/strong&gt; from the &lt;strong&gt;LLM response&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can LLM Directly Call Agents?
&lt;/h3&gt;

&lt;p&gt;At present, &lt;strong&gt;LLMs cannot directly perform actions&lt;/strong&gt;. They can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Understand the input.&lt;/li&gt;
&lt;li&gt;Provide reasoning and structured outputs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;However, &lt;strong&gt;LLM&lt;/strong&gt; can &lt;strong&gt;instruct&lt;/strong&gt; at best &lt;strong&gt;what should happen&lt;/strong&gt; but &lt;strong&gt;cannot&lt;/strong&gt; interact with the environment directly to p*&lt;em&gt;erform those actions&lt;/em&gt;*. For instance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;LLM can generate an HTML&lt;/strong&gt; response but &lt;strong&gt;cannot open a browser&lt;/strong&gt; to display it.&lt;/li&gt;
&lt;li&gt;It can suggest sending an email but cannot send it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To bridge this gap, we need &lt;strong&gt;output agents&lt;/strong&gt;. These agents read the LLM's output and perform the necessary actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Output Agent
&lt;/h3&gt;

&lt;p&gt;If the LLM generates an HTML response, an &lt;strong&gt;output agent&lt;/strong&gt; can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open a web browser or embed a browser within an application.&lt;/li&gt;
&lt;li&gt;Render the HTML content for the user to view.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How Agents Extend the Capabilities of LLMs
&lt;/h2&gt;

&lt;p&gt;Agents serve as intermediaries that interact with the LLM to sense the environment or execute actions.&lt;/p&gt;

&lt;p&gt;To make the LLM's outputs actionable, &lt;strong&gt;we need to augment the system with agents&lt;/strong&gt; that can process inputs going into the LLM and/or process output from the LLM to make it perform any actions.&lt;/p&gt;

&lt;p&gt;Let's consider we are going to create an &lt;strong&gt;agent-based system&lt;/strong&gt; that leverages an LLM to &lt;strong&gt;function&lt;/strong&gt; as a &lt;strong&gt;humorous AI assistant&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this setup, we define two key agents: the Input Agent and the Output Agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flow of Request Handling
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;User submits a query.&lt;/li&gt;
&lt;li&gt;The Input Agent receives the query, appends a system prompt, and forwards it to the LLM.&lt;/li&gt;
&lt;li&gt;The LLM processes the prompt, understands the context, and generates a response accordingly.&lt;/li&gt;
&lt;li&gt;The Output Agent, which understands the LLM's response format, parses it and executes the necessary actions - such as rendering the output, speaking it aloud, or performing other predefined tasks.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;User → Input Agent → LLM → Output Agent&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Example:
&lt;/h3&gt;

&lt;p&gt;Let's consider an example where we instruct the LLM to entertain the user with a joke and format the response in HTML:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;System Prompt:&lt;/strong&gt; "You are a funny AI. Your job is to entertain users. Make sure your responses are in HTML format with inbuilt styles."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query:&lt;/strong&gt; "Tell me a joke."&lt;/p&gt;

&lt;p&gt;Input to LLM = "System Prompt + Query"&lt;/p&gt;

&lt;p&gt;The user submits the "Tell me a joke" query, Input Agents creates a Prompt to LLM by appending the "System Prompt with the User Query."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expected Output from LLM:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"font-family: Arial; color: blue;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Why don't scientists trust atoms? Because they make up everything!&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The LLM successfully generates the output, but displaying it as a webpage or further acting on this HTML requires an output agent.&lt;/p&gt;

&lt;p&gt;The output agent we created recognizes the HTML format in the response. thus rendering the output in a web browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we discussed so far is a simple agent system where each agent is hardwired to interact with the LLM in a predefined way. While this setup works for limited use cases, it becomes impractical as we introduce more agents and tasks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What if we want to &lt;strong&gt;add a new agent&lt;/strong&gt; that performs speech synthesis?&lt;/li&gt;
&lt;li&gt;What if &lt;strong&gt;multiple agents need to work together dynamically&lt;/strong&gt;, rather than following a fixed sequence?&lt;/li&gt;
&lt;li&gt;What if we need to &lt;strong&gt;support multiple LLMs&lt;/strong&gt; based on different tasks?&lt;/li&gt;
&lt;li&gt;How do we manage interdependencies between agents efficiently?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A manually hardcoded agent system is &lt;strong&gt;rigid and difficult to scale&lt;/strong&gt;. Each new agent requires manual integration, and there's no flexibility in execution. To build t*&lt;em&gt;ruly scalable AI applications&lt;/em&gt;&lt;em&gt;, we need a **dynamic system that can decide which agents to use, route tasks efficiently, and adapt to new capabilities&lt;/em&gt;*, all without modifying the entire workflow.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is where &lt;strong&gt;Orchestration&lt;/strong&gt; comes in, acting as the &lt;strong&gt;brain of the system&lt;/strong&gt;, intelligently managing agents, LLM interactions, and task execution.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Need for an Orchestrator
&lt;/h2&gt;

&lt;p&gt;Agents play a crucial role in AI systems by extending the capabilities of LLMs. However, since LLMs cannot directly call agents, we need a central component, an Orchestrator that connects the LLM with the required context and agents to complete a task.&lt;/p&gt;

&lt;h3&gt;
  
  
  List of Components in the Orchestrator System
&lt;/h3&gt;

&lt;p&gt;Assume we are going to create an orchestrator system that has the below components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Input Client:&lt;/strong&gt; Sends user queries to the Orchestrator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents:&lt;/strong&gt; Responsible for different tasks, categorized as:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Classifier Agent:&lt;/strong&gt; Determines the type of request and decides which agent should handle it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intermediate Agent:&lt;/strong&gt; Performs predefined tasks.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM:&lt;/strong&gt; Processes input queries and helps in decision-making.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;strong&gt;Orchestrator&lt;/strong&gt; itself runs as a &lt;strong&gt;daemon process&lt;/strong&gt;, exposing APIs that allow input clients, agents, and the LLM to &lt;strong&gt;register and interact&lt;/strong&gt; within the ecosystem.&lt;/p&gt;

&lt;p&gt;In this system, we assume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multiple input clients and agents&lt;/strong&gt; can register.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only one LLM&lt;/strong&gt; can be registered at a time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Registration Process
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input Clients&lt;/strong&gt; register with a unique ID so that the Orchestrator knows where to send responses once the request is fulfilled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM&lt;/strong&gt; is also registered, so agents and input clients can access it via the Orchestrator when needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents&lt;/strong&gt; register with:

&lt;ul&gt;
&lt;li&gt;A unique ID&lt;/li&gt;
&lt;li&gt;A description of the agent role&lt;/li&gt;
&lt;li&gt;An input schema specifying what kind of input agent expect to perform predefined task&lt;/li&gt;
&lt;li&gt;An output schema defining what agent will return after processing&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This metadata allows the LLM to determine whether the system has the necessary capability to process a given request.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Example Query Flow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;User Query:&lt;/strong&gt;&lt;br&gt;
"Take a snapshot from the dashboard camera, merge it with a selfie from the interior camera, and send it to Sridhar's email."&lt;/p&gt;
&lt;h3&gt;
  
  
  Agents &amp;amp; Their Responsibilities
&lt;/h3&gt;
&lt;h4&gt;
  
  
  [1] Camera Agent
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Role:&lt;/strong&gt; Captures snapshots from either the dashboard or interior camera.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; "I'm a Camera Agent. I can take snapshots from the dashboard or interior camera."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input Schema:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"takePicFromDashboard"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"takePicFromInterior"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;bool&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output Schema:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dashboardPicPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string|null"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"interiorPicPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string|null"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  [2] Photo Editor Agent
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Role:&lt;/strong&gt; Creates a collage from multiple photos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; "I'm a Photo Editor Agent. I can create a collage from multiple images."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input Schema:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"picPaths"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output Schema:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"collagePicPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string|null"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  [3] Email Agent
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Role:&lt;/strong&gt; Sends an email with multimedia attachments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; "I'm an Email Agent. I can query user contacts, accept multimedia content, and send an email."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input Schema:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"emailSender"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mimeType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output Schema:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isEmailSent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;bool&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How the Orchestrator Uses LLM to Execute the Plan
&lt;/h3&gt;

&lt;p&gt;When the user submits a query, the Orchestrator sends it to the LLM along with the list of registered agent descriptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example LLM Prompt to Generate an Execution Plan&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I want you to act as a planner. Given a user query, devise a plan to execute it.

You can find the list of attached agents to understand the system's capabilities.

Agent descriptions:
[  
  /* All agent descriptions will be added here */  
]

Input Query: "/* Replace user query here */"

Output Format:
[
  {
    "agentName": "string",
    "agentParam": { /* The input schema expected by the agent */ }
  },
  {
    "agentName": "string",
    "agentParam": { /* The input schema expected by the agent */ }
  }
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the LLM to analyze the system's capabilities and generate an execution plan.&lt;/p&gt;

&lt;p&gt;Since the Orchestrator understands the LLM's structured response, it carefully executes the agents in the correct order to fulfill the request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final Execution Flow&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Input Client → Orchestrator → LLM → Camera Agent → Photo Editor Agent → Email Agent → (Optional: LLM Summarizes Response)&lt;/p&gt;

&lt;p&gt;As AI systems evolve, orchestrators will become even more essential in bridging the gap between reasoning and action. But will AI systems ever replace human roles entirely?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Will AI Replace Humans?
&lt;/h2&gt;

&lt;p&gt;I see two major work streams emerging in AI:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Model Development:&lt;/strong&gt; Engineers and researchers focused on building more efficient and powerful AI models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent &amp;amp; Orchestration Systems:&lt;/strong&gt; People working on AI agents, orchestration, and external context integration, which often requires domain-specific knowledge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's say, we would like to build a video logging system which would be mounted on a car dashboard, an AI agent might be responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Periodically capturing photo feeds.&lt;/li&gt;
&lt;li&gt;Geo-tagging them with location data.&lt;/li&gt;
&lt;li&gt;And converting the data into embeddings for an LLM to process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this case, the agent must &lt;strong&gt;understand the domain&lt;/strong&gt;, including &lt;strong&gt;how to interact with the camera, read GPS data, and structure the output&lt;/strong&gt; for further AI-driven analysis. This is where human expertise in &lt;strong&gt;software, hardware, and system integration&lt;/strong&gt; remains essential.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI's Role in the Workforce
&lt;/h3&gt;

&lt;p&gt;AI is already transforming the workforce by automating mundane and repetitive tasks. However,** AI is not yet at a stage where it can function independently**, for at least the next 5–10 years, human involvement will be crucial in guiding AI systems, making them more useful, adaptable, and safe.&lt;/p&gt;

&lt;p&gt;For instance, while AI can generate code, we are still far from &lt;strong&gt;an AI-driven system that can write, deploy, and manage software autonomously&lt;/strong&gt;, such as using AWS APIs to deploy a new system without any human intervention. &lt;strong&gt;Until AI can reliably handle real-world complexity, human oversight will be crucial.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;To ensure scalability, an Orchestrator system is essential. It seamlessly integrates various agents, classifiers, and LLMs, efficiently managing request routing, coordinating multiple agents, and utilizing the LLM at different stages of the workflow to accomplish a desired task.&lt;/p&gt;

&lt;p&gt;While there are open-source agent orchestration systems available, I haven't explored them yet. For our specific use case, we built a &lt;strong&gt;custom Orchestrator&lt;/strong&gt; tailored to our requirements. Once I've had the opportunity to evaluate other solutions, I'll be able to provide more insights into the existing open-source options.&lt;/p&gt;




&lt;h2&gt;
  
  
  Annexure
&lt;/h2&gt;

&lt;p&gt;PS: I used ChatGPT and QuillBot's grammar check to refine my writing.&lt;br&gt;
&lt;a href="https://quillbot.com/grammar-check" rel="noopener noreferrer"&gt;https://quillbot.com/grammar-check&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>Getting Started with Android Testing: Building Reliable Apps with Confidence (Part 3/3)</title>
      <dc:creator>Sridhar Subramani</dc:creator>
      <pubDate>Mon, 06 Jan 2025 15:16:55 +0000</pubDate>
      <link>https://dev.to/sridharsubramani/getting-started-with-android-testing-building-reliable-apps-with-confidence-part-33-2gj3</link>
      <guid>https://dev.to/sridharsubramani/getting-started-with-android-testing-building-reliable-apps-with-confidence-part-33-2gj3</guid>
      <description>&lt;h4&gt;
  
  
  Learn the Fundamentals of Android Testing, One Step at a Time (Part 3/3)
&lt;/h4&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://dev.to/sridharsubramani/getting-started-with-android-testing-building-reliable-apps-with-confidence-29m7"&gt;Previous article&lt;/a&gt;
&lt;/h4&gt;




&lt;h2&gt;
  
  
  Target Audience for This Blog
&lt;/h2&gt;

&lt;p&gt;This blog covers the basics of testing in Android, providing insights into setup, dependencies, and an introduction to different types of tests. It is designed to help beginners understand the fundamentals of Android testing and how various tests are implemented.&lt;/p&gt;




&lt;h2&gt;
  
  
  Integration testing
&lt;/h2&gt;

&lt;p&gt;Integration testing typically involves testing the interactions between different components or modules of an application.&lt;/p&gt;

&lt;p&gt;During these tests, we can visually observe the app launching, with all the interactions specified in the code happening in real time.&lt;/p&gt;

&lt;p&gt;However, there’s an alternative approach that leverages &lt;code&gt;GradleManagedDevices&lt;/code&gt; to run integration tests. This method skips the UI preview and executes the tests on a configured virtual or physical device. More details on this approach are provided in the next section.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integration Testing Frameworks
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Robolectric&lt;/td&gt;
&lt;td&gt;To perform android UI/functional testing on JVM without the need for android device.&lt;br&gt; * Test files are located inside the test folder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AndroidX test runner&lt;/td&gt;
&lt;td&gt;Provides AndroidJUnitRunner which is a JUnit test runner that allows to run instrumented JUnit 4 tests on Android devices, including those using the Espresso, UI Automator, and Compose testing frameworks.&lt;br&gt; * Test files are located inside the androidTest folder.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI Automator&lt;/td&gt;
&lt;td&gt;A UI testing framework designed for cross-app functional testing, enabling interactions with both system apps and installed apps. &lt;br&gt; * Test files are located inside the androidTest folder.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The following test cases, written for &lt;code&gt;RobolectricTestRunner&lt;/code&gt; and &lt;code&gt;AndroidJUnitRunner&lt;/code&gt;, appear similar to the Compose UI Unit Test code snippet. This is because the &lt;code&gt;androidx.compose.ui.test.junit4&lt;/code&gt; library provides test implementations for both JVM and Android. Using the same interfaces, tests can run on either runtime. The appropriate implementation is selected at runtime based on the configured test runner.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;androidx.compose.ui.test.junit4&lt;/code&gt; module provides the &lt;code&gt;ComposeTestRule&lt;/code&gt; and its Android-specific implementation, &lt;code&gt;AndroidComposeTestRule&lt;/code&gt;. These rules allow you to set Compose content or access the activity. You can construct these rules using factory functions: &lt;code&gt;createComposeRule&lt;/code&gt; for general use or &lt;code&gt;createAndroidComposeRule&lt;/code&gt; if activity access is required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Robolectric
&lt;/h2&gt;

&lt;p&gt;In this test, we are verifying the behavior of the &lt;strong&gt;Login&lt;/strong&gt; composable screen by ensuring that the login button is&lt;br&gt;
enabled only when the inputs provided by the user are valid.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initial State Validation:&lt;/strong&gt; The test confirms that the login button is initially disabled when no inputs are
provided.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial Input Validation:&lt;/strong&gt; The test simulates entering invalid email and password combinations step-by-step to
ensure that the button remains disabled until all conditions for validity are met.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Valid Input Validation:&lt;/strong&gt; Finally, the test validates that the login button becomes enabled only when both the
email and password meet the required validation criteria (a valid email format and a password of sufficient length).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This test ensures that the &lt;strong&gt;Login&lt;/strong&gt; composable correctly enforces input validation and enables the login button only under valid conditions.&lt;/p&gt;
&lt;h3&gt;
  
  
  System Under Test
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Composable
fun Login(onSuccess: (email: Email) -&amp;gt; Unit, viewModel: LoginViewModel = hiltViewModel()) {

    LaunchedEffect(key1 = viewModel.loginState, block = {
        if (viewModel.loginState == LoginState.LoginSuccess) onSuccess(viewModel.email)
    })

    Column {
        Text(text = stringResource(id = R.string.login))

        EmailInput(modifier = Modifier
            .semantics { testTagsAsResourceId = true;testTag = "emailInput" }
            .testTag("emailInput")
            .fillMaxWidth(),
            value = viewModel.email.value ?: "",
            isEnabled = viewModel.loginState !== LoginState.InProgress,
            onValueChange = viewModel::updateEmail)

        PasswordInput(modifier = Modifier
            .semantics { testTagsAsResourceId = true;testTag = "passwordInput" }
            .fillMaxWidth(),
            value = viewModel.password.value ?: "",
            isEnabled = viewModel.loginState !== LoginState.InProgress,
            onValueChange = viewModel::updatePassword)

        if (viewModel.loginState === LoginState.LoginPending) {
            PrimaryButton(modifier = Modifier
                .semantics { testTagsAsResourceId = true;testTag = "loginButton" }
                .fillMaxWidth(),
                text = stringResource(id = R.string.login),
                enabled = viewModel.isLoginButtonEnabled,
                onClick = viewModel::login)
        }

        if (viewModel.loginState === LoginState.InProgress) {
            CircularProgressIndicator(
                modifier = Modifier
                    .semantics { testTagsAsResourceId = true;testTag = "progressLoader" }
                    .align(Alignment.CenterHorizontally)
            )
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Test
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class LoginKtTest {

  @get:Rule
  val composeRule = createComposeRule()

  @Test
  fun shouldEnableButtonOnlyWhenInputsAreValid() {

    val loginUseCase = mockk&amp;lt;LoginUseCaseImpl&amp;gt;(relaxed = true)
    val loginViewModel = LoginViewModel(loginUseCase)

    coEvery { loginUseCase.login(any(), any()) } returns Unit

    with(composeRule) {
      setContent { Login(onSuccess = {}, viewModel = loginViewModel) }
      // Initial State Validation  
      onNodeWithTag("loginButton").assertIsNotEnabled()

      // Partial Input Validation
      onNodeWithTag("emailInput").performTextInput("abcd")
      onNodeWithTag("loginButton").assertIsNotEnabled()

      // Partial Input Validation  
      onNodeWithTag("emailInput").performTextInput("abcd@gmail.com")
      onNodeWithTag("loginButton").assertIsNotEnabled()

      // Partial Input Validation  
      onNodeWithTag("passwordInput").performTextInput("12")
      onNodeWithTag("loginButton").assertIsNotEnabled()

      // Valid Input Validation  
      onNodeWithTag("passwordInput").performTextInput("12345")
      onNodeWithTag("loginButton").assertIsEnabled()
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/sridhar-sp/android-test/blob/main/docs/LoginTest.gif" rel="noopener noreferrer"&gt;Click here to see in action&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Dependencies
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Allows us to create and configure mock objects, stub methods, verify method invocations, and more
androidTestImplementation("io.mockk:mockk-agent:1.13.5")
androidTestImplementation("io.mockk:mockk-android:1.13.5")
androidTestImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")

// Assertion library
androidTestImplementation("com.google.truth:truth:1.1.4")

// Needed for createComposeRule , createAndroidComposeRule and other rules used to perform UI test
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version") // used with AndroidTestRunner to run ui test on virtual/physical device.

// Required to add androidx.activity.ComponentActivity to test manifest.
// Needed for createComposeRule(), but not for createAndroidComposeRule&amp;lt;YourActivity&amp;gt;():
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;app/src/test/resources/robolectric.properties&lt;/code&gt; file and define the robolectric properties.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;instrumentedPackages&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;androidx.loader.content&lt;/span&gt;
&lt;span class="py"&gt;application&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;dagger.hilt.android.testing.HiltTestApplication&lt;/span&gt;
&lt;span class="py"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;29&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Android JUnit test
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AndroidJUnitRunner&lt;/code&gt; is a test runner which lets us run the test on android virtual/physical/GradleManaged devices, including those using the &lt;code&gt;Espresso&lt;/code&gt;, &lt;code&gt;UI Automator&lt;/code&gt;, and &lt;code&gt;Compose&lt;/code&gt; testing frameworks.&lt;/p&gt;

&lt;h3&gt;
  
  
  System Under Test
&lt;/h3&gt;

&lt;p&gt;We have two composable screens: &lt;code&gt;LoginScreen&lt;/code&gt; and &lt;code&gt;ProfileScreen&lt;/code&gt;, both inside &lt;a href="https://github.com/sridhar-sp/android-test/blob/main/app/src/main/kotlin/com/gandiva/android/sample/presentation/ui/main/MainScreen.kt" rel="noopener noreferrer"&gt;&lt;code&gt;MainScreen&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;LoginScreen&lt;/code&gt; contains:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;email&lt;/code&gt; and &lt;code&gt;password&lt;/code&gt; input fields&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;submit&lt;/code&gt; button&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Functionality:

&lt;ul&gt;
&lt;li&gt;Pressing the &lt;code&gt;submit&lt;/code&gt; button navigates the user to the &lt;code&gt;ProfileScreen&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ProfileScreen&lt;/code&gt; greets the user with a message displaying their &lt;code&gt;email&lt;/code&gt; ID.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@HiltAndroidTest
class MainScreenTest {

    @get:Rule(order = 0)
    val hiltAndroidRule = HiltAndroidRule(this)

    /**
     * Need a activity that annotated with @AndroidEntryPoint. and it has to be registered in manifest.
     * Add comment why we used createAndroidComposeRule instead of composeTestRule
     */
    @get:Rule(order = 1)
    val androidComposeRule = createAndroidComposeRule&amp;lt;DummyTestActivity&amp;gt;()

    @Test
    fun shouldSuccessfullyLaunchProfileScreenWithEmailPostLogin() {

        with(androidComposeRule) {
            setContent { MainScreen() }

            onNodeWithTag("emailInput").performTextInput("abc@gmail.com")
            onNodeWithTag("passwordInput").performTextInput("12345")
            onNodeWithTag("loginButton").performClick()

            waitUntil(2500L) {
                onAllNodesWithTag("welcomeMessageText").fetchSemanticsNodes().isNotEmpty()
            }

            onNodeWithTag("welcomeMessageText").assertTextEquals("Email as explicit argument abc@gmail.com")
            onNodeWithTag("welcomeMessageText2")
                .assertTextEquals("Email from saved state handle abc@gmail.com")

            waitForIdle()
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/sridhar-sp/android-test/blob/main/docs/MainScreenTest.gif" rel="noopener noreferrer"&gt;Click here to see in action&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Used to create AndroidHiltTestRunner from AndroidJUnitRunner
androidTestImplementation("androidx.test:runner:1.6.2")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;android&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="nf"&gt;defaultConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"&lt;/span&gt;

    &lt;span class="c1"&gt;// If we are using Hilt we can extend the AndroidJUnitRunner and pass the HiltTestApplication as application component.&lt;/span&gt;
    &lt;span class="n"&gt;testInstrumentationRunner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.gandiva.android.sample.AndroidHiltTestRunner"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AndroidHiltTestRunner&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AndroidJUnitRunner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;newApplication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ClassLoader&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="n"&gt;className&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;?):&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newApplication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HiltTestApplication&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  UI Automator
&lt;/h2&gt;

&lt;p&gt;UI Automator is a UI testing framework designed for cross-app functional UI testing, allowing interaction with both system and installed apps. Unlike frameworks that are limited to the app under test, UI Automator provides a wide range of APIs to interact with the entire device.&lt;/p&gt;

&lt;p&gt;This enables true cross-app functional testing, such as opening the device settings, disabling the network, and then launching your app to verify how it handles a no-network condition.&lt;/p&gt;

&lt;p&gt;With UI Automator, you can easily locate UI components using convenient descriptors like the text displayed on the component or its content description, making test scripts more intuitive and readable.&lt;/p&gt;

&lt;h3&gt;
  
  
  System Under Test
&lt;/h3&gt;

&lt;p&gt;We use an Android device as the system under test. The process begins by launching the home intent, bringing the device to the home screen. Once the home app is launched, we proceed to open our app for testing.&lt;/p&gt;

&lt;p&gt;The test scenario remains the same: we navigate to the &lt;code&gt;LoginScreen&lt;/code&gt;, enter the &lt;code&gt;email&lt;/code&gt; and &lt;code&gt;password&lt;/code&gt;, and press the&lt;br&gt;
submit button. Upon successful submission, the app navigates to the &lt;code&gt;ProfileScreen&lt;/code&gt;, where the user is greeted with their &lt;code&gt;email&lt;/code&gt; ID.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;BASIC_SAMPLE_PACKAGE&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.gandiva.android.sample"&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;LAUNCH_TIMEOUT&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000L&lt;/span&gt;

&lt;span class="nd"&gt;@HiltAndroidTest&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoginJourneyTest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;lateinit&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UiDevice&lt;/span&gt;

    &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;Rule&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;hiltAndroidRule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HiltAndroidRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@Before&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;startMainActivityFromHomeScreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Initialize UiDevice instance&lt;/span&gt;
        &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UiDevice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;InstrumentationRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInstrumentation&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

        &lt;span class="c1"&gt;// Start from the home screen&lt;/span&gt;
        &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pressHome&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;// Wait for launcher&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;launcherPackage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launcherPackageName&lt;/span&gt;
        &lt;span class="nc"&gt;MatcherAssert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertThat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;launcherPackage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;CoreMatchers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notNullValue&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Until&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;launcherPackage&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nc"&gt;LAUNCH_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Launch the app&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApplicationProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getApplicationContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;intent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packageManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLaunchIntentForPackage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BASIC_SAMPLE_PACKAGE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;addFlags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FLAG_ACTIVITY_CLEAR_TASK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Wait for the app to appear&lt;/span&gt;
        &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Until&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BASIC_SAMPLE_PACKAGE&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nc"&gt;LAUNCH_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;shouldLaunchProfileScreenWhenLoginIsSuccess&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enterTextOnFieldWithId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"emailInput"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"hello@gmail.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enterTextOnFieldWithId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"passwordInput"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"123456"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Until&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;res&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"loginButton"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="mi"&gt;1500L&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;assertThat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;res&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"loginButton"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="n"&gt;isEnabled&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isEqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clickFieldWithId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"loginButton"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Until&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;res&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hasObject"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="mi"&gt;3000L&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nf"&gt;assertThat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;textFromFieldWithId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"welcomeMessageText"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Email as explicit argument hello@gmail.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;assertThat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;textFromFieldWithId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"welcomeMessageText2"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Email from saved state handle hello@gmail.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForIdle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/sridhar-sp/android-test/blob/main/docs/JourneyTest.gif" rel="noopener noreferrer"&gt;Click here to see in action&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// To perform UI automation test.
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h3&gt;
  
  
  Command
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew connectedAndroidTest &lt;span class="nt"&gt;--continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Gradle Managed Devices
&lt;/h2&gt;

&lt;p&gt;Gradle Managed Devices provide a way to configure virtual or physical devices directly in Gradle for running integration tests. Since the configuration is managed within Gradle, it gains full control over the device lifecycle, allowing it to start or shut down devices as needed.&lt;/p&gt;

&lt;p&gt;Unlike standard Android Virtual Devices (AVDs) or physical devices, there won’t be any visual preview during the test run. Once the test completes, you can review the results in the reports generated in the build folder.&lt;/p&gt;

&lt;p&gt;Gradle Managed Devices are primarily used for running automated tests at scale on various virtual devices, so the focus is on configuration details rather than a visual representation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;testOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;managedDevices&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;devices&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ManagedVirtualDevice&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"testDevice"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Pixel 6"&lt;/span&gt;
        &lt;span class="n"&gt;apiLevel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;34&lt;/span&gt;
        &lt;span class="n"&gt;systemImageSource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"aosp"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Command
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew testDeviceDebugAndroidTest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Source Code
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/sridhar-sp/android-test" rel="noopener noreferrer"&gt;https://github.com/sridhar-sp/android-test&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Test Your Code, Rest Your Worries
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;With a sturdy suite of tests as steadfast as a fortress, developers can confidently push code even on a Friday evening and log off without a trace of worry.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>android</category>
      <category>testing</category>
      <category>kotlin</category>
      <category>compose</category>
    </item>
    <item>
      <title>Getting Started with Android Testing: Building Reliable Apps with Confidence (Part 2/3)</title>
      <dc:creator>Sridhar Subramani</dc:creator>
      <pubDate>Mon, 06 Jan 2025 05:25:11 +0000</pubDate>
      <link>https://dev.to/sridharsubramani/getting-started-with-android-testing-building-reliable-apps-with-confidence-29m7</link>
      <guid>https://dev.to/sridharsubramani/getting-started-with-android-testing-building-reliable-apps-with-confidence-29m7</guid>
      <description>&lt;h4&gt;
  
  
  Learn the Fundamentals of Android Testing, One Step at a Time (Part 2/3)
&lt;/h4&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://dev.to/sridharsubramani/getting-started-with-android-testing-building-reliable-apps-with-confidence-153m"&gt;Previous article&lt;/a&gt;
&lt;/h4&gt;




&lt;h2&gt;
  
  
  Target Audience for This Blog
&lt;/h2&gt;

&lt;p&gt;This blog covers the basics of testing in Android, providing insights into setup, dependencies, and an introduction to different types of tests. It is designed to help beginners understand the fundamentals of Android testing and how various tests are implemented.&lt;/p&gt;




&lt;h2&gt;
  
  
  UI Testing
&lt;/h2&gt;

&lt;p&gt;UI testing usually refers testing the user interface by simulating user action and verify the behavior of UI elements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Famous UI Testing Frameworks
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Espresso&lt;/td&gt;
&lt;td&gt;Android UI test framework to perform UI interaction and state assertion.  (White box testing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI Automator&lt;/td&gt;
&lt;td&gt;To perform cross-app functional UI testing across system and installed apps. (Both Black box &amp;amp; white box testing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compose UI test Junit&lt;/td&gt;
&lt;td&gt;To provide Junit rules invoke composable function in Junit. also provides APIs to perform UI interaction and state assertion.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Compose UI + Interaction Unit Test
&lt;/h2&gt;

&lt;p&gt;The Compose UI test framework allows you to verify that the behavior of your Compose code works as expected. It provides&lt;br&gt;
a set of testing APIs that help you find UI elements, check their attributes, and perform user actions. Using these APIs, you can mount composable content and assert expected behaviors.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;androidx.compose.ui.test.junit4&lt;/code&gt; module includes a &lt;code&gt;ComposeTestRule&lt;/code&gt; and an implementation for Android called &lt;code&gt;AndroidComposeTestRule&lt;/code&gt;. Through this rule you can set Compose content or access the activity. You construct the rules using factory functions, either &lt;code&gt;createComposeRule&lt;/code&gt; or, if you need access to an activity, &lt;code&gt;createAndroidComposeRule&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For Compose UI Unit Tests, you can use the &lt;code&gt;RobolectricTestRunner&lt;/code&gt;, a JUnit Test Runner that runs test code directly on the &lt;code&gt;JVM&lt;/code&gt;. This eliminates the need for a physical or virtual Android device, significantly speeding up test execution, ensuring consistent results, and simplifying the testing process.&lt;/p&gt;

&lt;p&gt;However, some classes and methods from &lt;code&gt;android.jar&lt;/code&gt; require additional configuration to function correctly. For example, accessing Android resources or using methods like Log might need adjustments to return default or mocked values. Please refer to the setup section below for the necessary configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;

&lt;p&gt;In this test, we are verifying the behavior of the &lt;strong&gt;Login&lt;/strong&gt; composable screen by ensuring that the login button is&lt;br&gt;
enabled only when the inputs provided by the user are valid.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Initial State Validation:&lt;/strong&gt; The test confirms that the login button is initially disabled when no inputs are provided.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Partial Input Validation:&lt;/strong&gt; The test simulates entering invalid email and password combinations step-by-step to ensure that the button remains disabled until all conditions for validity are met.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Valid Input Validation:&lt;/strong&gt; Finally, the test validates that the login button becomes enabled only when both the email and password meet the required validation criteria (a valid email format and a password of sufficient length).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This test ensures that the &lt;strong&gt;Login&lt;/strong&gt; composable correctly enforces input validation and enables the login button only under valid conditions.&lt;/p&gt;

&lt;h4&gt;
  
  
  System Under Test
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Composable
fun Login(onSuccess: (email: Email) -&amp;gt; Unit, viewModel: LoginViewModel = hiltViewModel()) {

  LaunchedEffect(key1 = viewModel.loginState, block = {
    if (viewModel.loginState == LoginState.LoginSuccess) onSuccess(viewModel.email)
  })

  Column {
    Text(text = stringResource(id = R.string.login))

    EmailInput(modifier = Modifier
      .semantics { testTagsAsResourceId = true;testTag = "emailInput" }
      .testTag("emailInput")
      .fillMaxWidth(),
      value = viewModel.email.value ?: "",
      isEnabled = viewModel.loginState !== LoginState.InProgress,
      onValueChange = viewModel::updateEmail)

    PasswordInput(modifier = Modifier
      .semantics { testTagsAsResourceId = true;testTag = "passwordInput" }
      .fillMaxWidth(),
      value = viewModel.password.value ?: "",
      isEnabled = viewModel.loginState !== LoginState.InProgress,
      onValueChange = viewModel::updatePassword)

    if (viewModel.loginState === LoginState.LoginPending){
        PrimaryButton(modifier = Modifier
            .semantics { testTagsAsResourceId = true;testTag = "loginButton" }
            .fillMaxWidth(),
            text = stringResource(id = R.string.login),
            enabled = viewModel.isLoginButtonEnabled,
            onClick = viewModel::login)
    }

    if (viewModel.loginState === LoginState.InProgress){
        CircularProgressIndicator(
            modifier = Modifier
                .semantics { testTagsAsResourceId = true;testTag = "progressLoader" }
                .align(Alignment.CenterHorizontally)
        )
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Test
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;This is a Compose UI &lt;strong&gt;unit test&lt;/strong&gt; that runs on the &lt;strong&gt;JVM&lt;/strong&gt;. Therefore, the code must be placed inside &lt;code&gt;app/src/test/java/../LoginKtTest.kt&lt;/code&gt; .
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@RunWith(RobolectricTestRunner::class)
class LoginKtTest {

  @get:Rule
  val composeRule = createComposeRule()

  @get:Rule
  var mainCoroutineRule = MainCoroutineRule()

  @Test
  fun shouldEnableButtonOnlyWhenInputsAreValid() {
    with(composeRule) {
      val loginUseCase = mockk&amp;lt;LoginUseCaseImpl&amp;gt;()
      val loginViewModel = LoginViewModel(loginUseCase)
      setContent { Login(onSuccess = {}, viewModel = loginViewModel) }
      onNodeWithTag("loginButton").assertIsNotEnabled()

      onNodeWithTag("emailInput").performTextInput("abcd")
      onNodeWithTag("loginButton").assertIsNotEnabled()

      onNodeWithTag("emailInput").performTextInput("abcd@gmail.com")
      onNodeWithTag("loginButton").assertIsNotEnabled()

      onNodeWithTag("passwordInput").performTextInput("12")
      onNodeWithTag("loginButton").assertIsNotEnabled()

      onNodeWithTag("passwordInput").performTextInput("12345")
      onNodeWithTag("loginButton").assertIsEnabled()
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Needed for createComposeRule , createAndroidComposeRule and other rules used to perform UI test
testImplementation("androidx.compose.ui:ui-test-junit4:$compose_version") // used with robolectric to run ui test on jvm

// Needed for createComposeRule(), but not for createAndroidComposeRule&amp;lt;YourActivity&amp;gt;():
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")

// Dependency injection for For instrumented tests on JVM
testImplementation("com.google.dagger:hilt-android-testing:2.49")
kaptTest("com.google.dagger:hilt-compiler:2.49")

// Needed to run android UI test on JVM instead of on an emulator or device
testImplementation("org.robolectric:robolectric:4.10.3)

// Helper for other arch dependencies, including JUnit test rules that can be used with LiveData, coroutines etc
testImplementation("androidx.arch.core:core-testing:2.2.0")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;testOptions {
        unitTests {
            // Enables unit tests to use Android resources, assets, and manifests.
            isIncludeAndroidResources = true
            // Whether unmocked methods from android.jar should throw exceptions or return default values (i.e. zero or null).
            isReturnDefaultValues = true
        }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Command
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./gradlew testDebugUnitTest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Source Code
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/sridhar-sp/android-test" rel="noopener noreferrer"&gt;https://github.com/sridhar-sp/android-test&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  &lt;a href="https://dev.to/sridharsubramani/getting-started-with-android-testing-building-reliable-apps-with-confidence-part-33-2gj3"&gt;Next Article&lt;/a&gt;
&lt;/h3&gt;




&lt;h3&gt;
  
  
  Test Your Code, Rest Your Worries
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;With a sturdy suite of tests as steadfast as a fortress, developers can confidently push code even on a Friday evening and log off without a trace of worry.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>android</category>
      <category>testing</category>
      <category>kotlin</category>
      <category>compose</category>
    </item>
    <item>
      <title>Getting Started with Android Testing: Building Reliable Apps with Confidence (Part 1/3)</title>
      <dc:creator>Sridhar Subramani</dc:creator>
      <pubDate>Sun, 05 Jan 2025 15:09:22 +0000</pubDate>
      <link>https://dev.to/sridharsubramani/getting-started-with-android-testing-building-reliable-apps-with-confidence-153m</link>
      <guid>https://dev.to/sridharsubramani/getting-started-with-android-testing-building-reliable-apps-with-confidence-153m</guid>
      <description>&lt;h4&gt;
  
  
  Learn the Fundamentals of Android Testing, One Step at a Time Part 1/3
&lt;/h4&gt;




&lt;h2&gt;
  
  
  Target Audience for This Blog
&lt;/h2&gt;

&lt;p&gt;This blog covers the basics of testing in Android, providing insights into setup, dependencies, and an introduction to different types of tests. It is designed to help beginners understand the fundamentals of Android testing and how various tests are implemented.&lt;/p&gt;




&lt;h2&gt;
  
  
  Different Types of Test
&lt;/h2&gt;

&lt;p&gt;The following are the primary testing types that are commonly used in software products:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unit testing&lt;/li&gt;
&lt;li&gt;UI testing&lt;/li&gt;
&lt;li&gt;Integration testing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Place of Execution
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Execution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit testing&lt;/td&gt;
&lt;td&gt;JVM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI testing&lt;/td&gt;
&lt;td&gt;JVM or Android device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration testing&lt;/td&gt;
&lt;td&gt;Android device&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Unit Testing
&lt;/h2&gt;

&lt;p&gt;Unit testing usually refers to testing a particular unit of code in complete isolation from other components to ensure its correctness and functionality. Developers often use frameworks like &lt;code&gt;Mockito&lt;/code&gt; to create stubs (test doubles), mocks, etc., to achieve this isolation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stub&lt;/strong&gt;: A stub is a direct replacement for a function, interface, or abstract class (or any other dependency). It&lt;br&gt;
allows us to swap the original implementation with a test-specific version, often referred to as a test dummy (or test&lt;br&gt;
double).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mock&lt;/strong&gt;: A mock serves as a more advanced test double for a dependency. Mocking frameworks let us actively simulate different behaviours by configuring the mock to return specific responses based on inputs or conditions. Furthermore, mocks allow us to confirm interactions by verifying the existence of a method, its number of calls, and the arguments passed during each call.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Why do we need this?&lt;/strong&gt; During testing, especially unit testing, we aim to isolate the component under test from its dependencies. This ensures that we're testing the component alone, making the tests simpler, faster, and less error-prone. Mocking or stubbing helps us avoid injecting side effects or relying on external dependencies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: Imagine a ViewModel class that depends on a repository. The Repository class, in turn, makes network API calls. If we want to write a unit test for the ViewModel alone, we don’t want to incur the overhead of making actual API calls, as this can make the test error-prone due to network conditions or server response times. To avoid these side effects, we can replace the repository with a stub (test double) or a mock during the test. This ensures that we focus only on the behaviour of the ViewModel while bypassing external dependencies.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Famous Unit Testing Frameworks
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Junit&lt;/td&gt;
&lt;td&gt;Testing framework for Java&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mockito&lt;/td&gt;
&lt;td&gt;Mocking framework for unit tests written in Java/Kotlin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Truth&lt;/td&gt;
&lt;td&gt;To perform assertions in tests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Simple Test Without Mocks
&lt;/h2&gt;

&lt;p&gt;In this test suite, we are validating the behavior of the &lt;code&gt;isValid()&lt;/code&gt; method in the &lt;code&gt;Email&lt;/code&gt; class. The &lt;code&gt;isValid()&lt;/code&gt;&lt;br&gt;
method checks whether the email provided is a valid email address or not. We are testing three key scenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Null Email:&lt;/strong&gt; Verifying that when the email value is &lt;code&gt;null&lt;/code&gt;, the method returns &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Invalid Email:&lt;/strong&gt; Checking various invalid email formats to ensure that the method correctly returns &lt;code&gt;false&lt;/code&gt; for them (e.g., missing domain, misplaced characters).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Valid Email:&lt;/strong&gt; Confirming that the method correctly returns &lt;code&gt;true&lt;/code&gt; for properly formatted email addresses.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each test ensures the &lt;code&gt;isValid()&lt;/code&gt; method behaves as expected under different conditions, guaranteeing that the email validation works correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  System Under Test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data class Email(val value: String?) : Parcelable {
    fun isValid(): Boolean {
        return if (value == null) false else PatternsCompat.EMAIL_ADDRESS.matcher(value).matches()
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmailTest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;shouldReturnIsValidAsFalseWhenEmailIsNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Truth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertThat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;isFalse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;shouldReturnIsValidAsFalseWhenEmailIsInvalid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Truth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertThat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"aa@.com"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;isFalse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nc"&gt;Truth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertThat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"aacd@aa.com@"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;isFalse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nc"&gt;Truth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertThat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;isFalse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nc"&gt;Truth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertThat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@gmail.com"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;isFalse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;shouldReturnIsValidAsTrueWhenEmailIsValid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Truth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertThat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"abcd@domain.com"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;isTrue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nc"&gt;Truth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertThat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a@domain.in"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;isTrue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Simple Test With Mocks
&lt;/h2&gt;

&lt;p&gt;In this test, we are verifying the behavior of the &lt;code&gt;ProfileViewModel&lt;/code&gt; class, specifically the retrieval of the email address from the &lt;code&gt;SavedStateHandle&lt;/code&gt;. The test mocks the &lt;code&gt;SavedStateHandle&lt;/code&gt; to simulate retrieving an email address from the saved state.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mocking Dependencies:&lt;/strong&gt; We use &lt;code&gt;mockk&lt;/code&gt; to mock the &lt;code&gt;SavedStateHandle&lt;/code&gt; and &lt;code&gt;LogoutUseCase&lt;/code&gt;, which are dependencies in the &lt;code&gt;ProfileViewModel&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Testing Behavior:&lt;/strong&gt; The mock for &lt;code&gt;SavedStateHandle&lt;/code&gt; is configured to return a predefined email address &lt;code&gt;abcd@gmail.com&lt;/code&gt; when the &lt;code&gt;KEY_EMAIL&lt;/code&gt; key is accessed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validation:&lt;/strong&gt; After initializing the &lt;code&gt;ProfileViewModel&lt;/code&gt;, we assert that the &lt;code&gt;emailAddress&lt;/code&gt; property correctly retrieves the mocked email value from the &lt;code&gt;SavedStateHandle&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This test ensures that the &lt;code&gt;ProfileViewModel&lt;/code&gt; correctly reads the email address from the saved state during its initialization.&lt;/p&gt;

&lt;h3&gt;
  
  
  System Under Test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@HiltViewModel&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProfileViewModel&lt;/span&gt; &lt;span class="nd"&gt;@Inject&lt;/span&gt; &lt;span class="k"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;savedStateHandle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SavedStateHandle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;logoutUseCase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;LogoutUseCase&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;emailAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;savedStateHandle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nc"&gt;BundleArgs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;KEY_EMAIL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`should&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="nf"&gt;viewModel`&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;savedStateHandleMock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mockk&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SavedStateHandle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
  &lt;span class="n"&gt;every&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;savedStateHandleMock&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;BundleArgs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;KEY_EMAIL&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;returns&lt;/span&gt; &lt;span class="s"&gt;"abcd@gmail.com"&lt;/span&gt;

  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;logoutUseCase&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mockk&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LogoutUseCase&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;

  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;profileViewModel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ProfileViewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedStateHandleMock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logoutUseCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;assertThat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profileViewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emailAddress&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isEqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"abcd@gmail.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Test With Mocks and Stubs
&lt;/h2&gt;

&lt;p&gt;In this test, we are testing the behavior of the &lt;code&gt;ProfileViewModel&lt;/code&gt; class when the &lt;code&gt;logout&lt;/code&gt; function is called, ensuring that the logout process is correctly triggered and the &lt;code&gt;shouldLogout&lt;/code&gt; state is updated.&lt;/p&gt;

&lt;p&gt;Let me use &lt;code&gt;AAA&lt;/code&gt; test pattern to explain the test case. &lt;code&gt;AAA&lt;/code&gt; stands for &lt;strong&gt;Arrange&lt;/strong&gt;, &lt;strong&gt;Act&lt;/strong&gt;, and &lt;strong&gt;Assert&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Arrange:: Mocking and Stub Dependencies:&lt;/strong&gt; We mock the &lt;code&gt;SavedStateHandle&lt;/code&gt; to simulate retrieving the &lt;code&gt;email&lt;/code&gt; address from the saved state, and we stub the &lt;code&gt;LogoutUseCase&lt;/code&gt; to simulate a successful logout without performing the actual logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Act:: Triggering Logout:&lt;/strong&gt; The &lt;code&gt;logout&lt;/code&gt; function is called on the &lt;code&gt;ProfileViewModel&lt;/code&gt;, and the coroutine is run to completion using &lt;code&gt;runCurrent()&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Assert:&lt;/strong&gt; The test asserts that after calling &lt;code&gt;logout&lt;/code&gt;, the &lt;code&gt;shouldLogout&lt;/code&gt; state is updated to &lt;code&gt;true&lt;/code&gt; and that the &lt;code&gt;logout&lt;/code&gt; function was successfully called, as indicated by the &lt;code&gt;isLogoutSuccess&lt;/code&gt; flag being &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This test ensures that the &lt;code&gt;ProfileViewModel&lt;/code&gt; correctly handles the &lt;code&gt;logout&lt;/code&gt; process, updating the appropriate states and interacting with the &lt;code&gt;LogoutUseCase&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  System Under Test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@HiltViewModel&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProfileViewModel&lt;/span&gt; &lt;span class="nd"&gt;@Inject&lt;/span&gt; &lt;span class="k"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;savedStateHandle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SavedStateHandle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;logoutUseCase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;LogoutUseCase&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;emailAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;savedStateHandle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nc"&gt;BundleArgs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;KEY_EMAIL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;shouldLogout&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;

  &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;viewModelScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;logoutUseCase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emailAddress&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="n"&gt;shouldLogout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Test
fun `should call logout callback when logout button is pressed`() = runTest(testDispatcher) {
    Dispatchers.setMain(testDispatcher)

    // Arrange    
    val savedStateHandleMock = mockk&amp;lt;SavedStateHandle&amp;gt;()
    every&amp;lt;String?&amp;gt; { savedStateHandleMock[BundleArgs.KEY_EMAIL] } returns "abcd@gmail.com"

    var isLogoutSuccess = false
    val logoutStub = object : LogoutUseCase {
      override suspend fun logout(email: Email) {
        isLogoutSuccess = true
      }
    }

    val profileViewModel = ProfileViewModel(savedStateHandleMock, logoutStub)

    // Act    
    profileViewModel.logout()
    runCurrent()  // run current co routine to completion

    // Assert    
    assertThat(profileViewModel.shouldLogout).isTrue()
    assertThat(isLogoutSuccess).isTrue()
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Regular JUnit dependency
testImplementation("junit:junit:4.13.2")

// Assertion library
testImplementation("com.google.truth:truth:1.1.4")

// Allows us to create and configure mock objects, stub methods, verify method invocations, and more
testImplementation("io.mockk:mockk:1.13.5")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Command
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew testDebugUnitTest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Source Code
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/sridhar-sp/android-test" rel="noopener noreferrer"&gt;https://github.com/sridhar-sp/android-test&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Test Your Code, Rest Your Worries
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;With a sturdy suite of tests as steadfast as a fortress, developers can confidently push code even on a Friday evening and log off without a trace of worry.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  &lt;a href="https://dev.to/sridharsubramani/getting-started-with-android-testing-building-reliable-apps-with-confidence-29m7"&gt;Next Article&lt;/a&gt;
&lt;/h3&gt;

</description>
      <category>android</category>
      <category>testing</category>
      <category>kotlin</category>
      <category>unittest</category>
    </item>
    <item>
      <title>Beyond Boundaries: Android Inter-Process Communication with AIDL</title>
      <dc:creator>Sridhar Subramani</dc:creator>
      <pubDate>Sun, 29 Dec 2024 11:46:33 +0000</pubDate>
      <link>https://dev.to/sridharsubramani/beyond-boundaries-android-inter-process-communication-with-aidl-sridhar-subramaniam-301</link>
      <guid>https://dev.to/sridharsubramani/beyond-boundaries-android-inter-process-communication-with-aidl-sridhar-subramaniam-301</guid>
      <description>&lt;p&gt;A step-by-step guide to implementing reliable cross-app services&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s AIDL
&lt;/h2&gt;

&lt;p&gt;AIDL (Android Interface Definition Language) is used to create a common interface for client-server communication in Android.&lt;/p&gt;

&lt;p&gt;Both client and server can agree on the common interface to communicate with each other using IPC.&lt;/p&gt;

&lt;p&gt;AIDL supports all Java primitive data types and a handful of wrapper data types, such as String, List, Map, Parcelable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Android IPC
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Android apps can communicate using several methods; this guide focuses on IPC via AIDL.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What we are going to build
&lt;/h3&gt;

&lt;p&gt;To demonstrate AIDL in action, we’ll build a practical example: a sensor data logging system consisting of two applications:&lt;/p&gt;

&lt;h3&gt;
  
  
  Server App:
&lt;/h3&gt;

&lt;p&gt;A service that reads various sensors and provides APIs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetch real-time sensor values&lt;/li&gt;
&lt;li&gt;Register callbacks for sensor value changes&lt;/li&gt;
&lt;li&gt;Manage logging sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Client App:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Connects to the server app and consumes the sensor data&lt;/li&gt;
&lt;li&gt;The client app will connect to the server app by binding with the service; once binding is complete, we get a binder object, which is the implementation of the AIDL interface we both agreed on. using this binder, we can call the API exposed from Server app&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Service Connector
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;We will also discuss a small utility class that I created to take care of the boiler plate code when binding with a service.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ServiceConnector&lt;/code&gt; handles binding to services and manages retries when the server crashes or stops. It provides a &lt;code&gt;getService&lt;/code&gt; method that ⁣&lt;code&gt;suspends&lt;/code&gt; until a connection is established or a timeout occurs.&lt;/li&gt;
&lt;li&gt;This will come in handy when the server app crashes/stops due to some reason. The next time we call the &lt;code&gt;getService&lt;/code&gt; method, it takes care of binding with the service and returns the binder interface.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Let’s see some code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create an AIDL interface
The files should have &lt;code&gt;.aidl&lt;/code&gt; extension and should be placed inside a &lt;code&gt;src/main/aidl&lt;/code&gt; folder
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package com.gandiva.aidl.remoteservices;
import com.gandiva.aidl.remoteservices.SensorDataCallback;

interface SensorDataLoggerService {
    String getSpeedInKm();
    int getRPM();
    void startLogging(in SensorDataCallback callback);
    void stopLogging(in SensorDataCallback callback);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package com.gandiva.aidl.remoteservices;
import com.gandiva.aidl.remoteservices.model.SensorData;

interface SensorDataCallback {
    // Create a SensorData class, which should implement android.os.Parcelable interface, and placed in java/kotlin package.
    void onEvent(in SensorData data);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create a SensorData class, which should implement the ⁣&lt;code&gt;android.os.Parcelable&lt;/code&gt; interface, and place it in the &lt;code&gt;Java/Kotlin&lt;/code&gt; package.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package com.gandiva.aidl.remoteservices.model

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
data class SensorData(val sensorID: Int, val value: Int) : Parcelable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;We need to share this interface with both applications, so it would be advisable to create a library module and share the interface with the two apps. Take a look at &lt;a href="https://github.com/sridhar-sp/android-playground/tree/main/AIDL/remoteServices" rel="noopener noreferrer"&gt;this Android library module&lt;/a&gt; for reference.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create an Android Service to expose the Binder interface implementation from onBind method
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class SensorDataLoggerServiceImpl : Service() {

  companion object {
    const val SENSOR_DATA_LOGGER_BIND_ACTION = "com.gandiva.aidl.server.action.BIND_SENSOR_DATA_LOGGER"
  }

  override fun onBind(intent: Intent): IBinder? {
    if (intent.action != SENSOR_DATA_LOGGER_BIND_ACTION) return null

    val binder = object : SensorDataLoggerService.Stub() {
      override fun getSpeedInKm(): String {
        TODO("Not yet implemented")
      }

      override fun getRPM(): Int {
        TODO("Not yet implemented")
      }

      override fun startLogging(callback: SensorDataCallback?) {
        TODO("Not yet implemented")
      }

      override fun stopLogging(callback: SensorDataCallback?) {
        TODO("Not yet implemented")
      }
    }
    binder.linkToDeath(DeathRecipient { Log.d("SensorDataLoggerService", "**** Service died") }, 0)
    return binder
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For brevity, the methods just have TODO; actual implementation can be found &lt;a href="https://github.com/sridhar-sp/android-playground/blob/main/AIDL/server/src/main/kotlin/com/gandiva/aidl/server/sensor/SensorDataLoggerServiceAidl.kt" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The client can try to bind the exposed service from the server app using explicit intent.&lt;/li&gt;
&lt;li&gt;Once the service is connected, we can typecast the IBinder instance to the AIDL interface type, which both apps agreed on step 1.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@HiltViewModel
class SensorDataLoggerViewModel @Inject constructor(val appContext: Application) : AndroidViewModel(appContext) {

  companion object {
    const val SENSOR_DATA_LOGGER_PKG_NAME = "com.gandiva.aidl.server"
    const val SENSOR_DATA_LOGGER_SERVICE_NAME = "com.gandiva.aidl.server.sensor.SensorDataLoggerServiceImpl"
    const val SENSOR_DATA_LOGGER_BIND_ACTION = "com.gandiva.aidl.server.action.BIND_SENSOR_DATA_LOGGER"
  }

  private var sensorDataLoggerService: SensorDataLoggerService? = null

  var isServiceConnected by mutableStateOf(false)
    private set

  private val serviceConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
      isServiceConnected = true
      sensorDataLoggerService = SensorDataLoggerService.Stub.asInterface(service)
    }

    override fun onServiceDisconnected(name: ComponentName?) {
      isServiceConnected = false
    }
  }

  fun disconnectService() {
    appContext.applicationContext.unbindService(serviceConnection)
    isServiceConnected = false
  }

  // Should call this method before accessing [sensorDataLoggerService]
  fun connectToService(appContext: Context = this.getApplication()) {
    val bindIntent = Intent().apply {
      component = ComponentName(SENSOR_DATA_LOGGER_PKG_NAME, SENSOR_DATA_LOGGER_SERVICE_NAME)
      action = SENSOR_DATA_LOGGER_BIND_ACTION
    }

    if (Build.VERSION.SDK_INT &amp;gt;= Build.VERSION_CODES.Q) {
      appContext.bindService(bindIntent, Context.BIND_AUTO_CREATE, appContext.mainExecutor, serviceConnection)
    } else {
      appContext.applicationContext.bindService(bindIntent, serviceConnection, Context.BIND_NOT_FOREGROUND)
    }
  }

  fun showSpeed() {
    val speedInKm = sensorDataLoggerService?.speedInKm // Assume service is connected
    Toast.makeText(appContext, "Speed $speedInKm", Toast.LENGTH_SHORT).show()
  }

  fun showRpm() {
    val rpm = sensorDataLoggerService?.rpm // Assume service is connected
    Toast.makeText(appContext, "RPM $rpm", Toast.LENGTH_SHORT).show()
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;In the above code, we have to explicitly call the ⁣&lt;code&gt;connectToService&lt;/code&gt; method to initialize the connection, and post connection only, we should call any method from the (AIDL) binder instance.&lt;/li&gt;
&lt;li&gt;So our code works under the assumption that when a &lt;code&gt;showSpeed&lt;/code&gt; method is called, it tries to call the AIDL API irrespective of the connection. This works in the best-case scenario, but the real world will be far from the best-case scenario. The server app can crash or stop, post-initial connection, leaving the last obtained AIDL binder instance as obsolete.&lt;/li&gt;
&lt;li&gt;So, we should have a way to call the API only when the service is connected; we can modify the showSpeed method like below to call the API when the service is connected, or call &lt;code&gt;connectToService&lt;/code&gt; and wait for the service connection; post we are eligible to call any API.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; fun showSpeed() {
  if (isServiceConnected) {
    val speedInKm = sensorDataLoggerService?.speedInKm
    Toast.makeText(appContext, "Speed $speedInKm", Toast.LENGTH_SHORT).show()
  } else {
    connectToService() // Async operation
    // Wait for service to connect then call API
    val speedInKm = sensorDataLoggerService?.speedInKm
    Toast.makeText(appContext, "Speed $speedInKm", Toast.LENGTH_SHORT).show()
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;But we have to follow the same approach everywhere before calling the AIDL API to handle the worst-case scenario. But if we do that, it will introduce a lot of boilerplate code.&lt;/li&gt;
&lt;li&gt;The best way to deal with this is to create a utility class that takes care of this complexity. Following is one example (i.e., &lt;code&gt;ServiceConnector&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Service connector
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ServiceConnector&lt;/code&gt; exposes a getService suspend function, which will suspend until a connection is made or timeOutInMillis expires.&lt;/li&gt;
&lt;li&gt;This handles the service connection and retry logic internally, so clients don’t have to worry about the service connection or retry in case the server died or crashed.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface IServiceConnector&amp;lt;T&amp;gt; {
  suspend fun getService(timeOutInMillis: Long = -1): T?

  suspend fun unbindService()

  fun onServiceConnected() {}
}

open class ServiceConnector&amp;lt;T&amp;gt;(
  private val context: Context,
  private val intent: Intent,
  val transformBinderToService: (service: IBinder?) -&amp;gt; T?,
  private val allowNullBinding: Boolean = false
) : IServiceConnector&amp;lt;T&amp;gt; {

  private var serviceConnected = false

  private var service: T? = null

  private val mutex = Mutex()

  private var lastServiceConnection: ServiceConnection? = null

  private val logTag = "Service :: ${this.javaClass}"

  override suspend fun getService(timeOutInMillis: Long): T? {
    // If allowNullBinding is true don't care what service object is
    if (serviceConnected &amp;amp;&amp;amp; (allowNullBinding || service != null)) {
      return service
    }

    if (timeOutInMillis &amp;lt; 0)
      return mutex.withLock { bindAndGetService() }
    return mutex.withLock { withTimeoutOrNull(timeOutInMillis) { bindAndGetService() } }
  }

  private suspend fun bindAndGetService() = suspendCancellableCoroutine { continuation -&amp;gt;
    val serviceConnection = object : ServiceConnection {
      override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        resumeWithServiceInstance(binder)
      }

      override fun onServiceDisconnected(name: ComponentName?) {
        cleanUpAndResumeIfRequired()
      }

      override fun onBindingDied(name: ComponentName?) {
        cleanUpAndResumeIfRequired()
      }

      override fun onNullBinding(name: ComponentName?) {
        if (allowNullBinding) resumeWithServiceInstance(null)
        else cleanUpAndResumeIfRequired()
      }

      private fun resumeWithServiceInstance(binder: IBinder?) {
        service = transformBinderToService(binder)
        serviceConnected = true
        if (continuation.isActive) continuation.resume(service)
        onServiceConnected()
      }

      private fun cleanUpAndResumeIfRequired() {
        service = null
        serviceConnected = false
        if (continuation.isActive) continuation.resume(null)
      }

    }

    context.bindService(
      intent, serviceConnection, Context.BIND_AUTO_CREATE
    )

    lastServiceConnection = serviceConnection
  }

  @Throws(Exception::class)
  override suspend fun unbindService() {
    lastServiceConnection?.let(context::unbindService)
    serviceConnected = false
    service = null
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Extend the &lt;code&gt;ServiceConnector&lt;/code&gt; and provide necessary information about the service to which we want to connect and the type of the binder interface.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context&lt;/code&gt; Context used to bind the service.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;intent&lt;/code&gt; Explicit intent describing the service to connect.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;transformBinderToService&lt;/code&gt; callback function called to transform the generic IBinder instance to the client-specific AIDL interface.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allowNullBinding&lt;/code&gt; Pass true to indicate to keep the server connected even if the server returns a null IBinder instance from the onBind method.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class SensorDataLoggerServiceCoordinator(context: Context) : ServiceConnector&amp;lt;SensorDataLoggerService&amp;gt;(
  context = context,
  intent = bindIntent(),
  transformBinderToService = { binder: IBinder? -&amp;gt; binder?.let { SensorDataLoggerService.Stub.asInterface(it) } },
  allowNullBinding = false
) {

  companion object {
    private const val SENSOR_DATA_LOGGER_PKG_NAME = "com.gandiva.aidl.server"
    private const val SENSOR_DATA_LOGGER_SERVICE_NAME = "com.gandiva.aidl.server.sensor.SensorDataLoggerServiceImpl"
    private const val SENSOR_DATA_LOGGER_BIND_ACTION = "com.gandiva.aidl.server.action.BIND_SENSOR_DATA_LOGGER"

    fun bindIntent(): Intent {
      return Intent().apply {
        component = ComponentName(SENSOR_DATA_LOGGER_PKG_NAME, SENSOR_DATA_LOGGER_SERVICE_NAME)
        action = SENSOR_DATA_LOGGER_BIND_ACTION
      }
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;To use create an instance of &lt;code&gt;SensorDataLoggerServiceCoordinator&lt;/code&gt; and use the &lt;code&gt;getService&lt;/code&gt; method to obtain the binder instance
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@HiltViewModel
class SensorDataLoggerViewModelV2 @Inject constructor(val appContext: Application) : AndroidViewModel(appContext) {

  private val sensorDataLoggerServiceCoordinator: SensorDataLoggerServiceCoordinator by lazy {
    SensorDataLoggerServiceCoordinator(context = appContext)
  }

  fun showSpeed() {
    viewModelScope.launch {
      // Suspend till service gets connected.
      val speedInKm = sensorDataLoggerServiceCoordinator.getService()?.speedInKm
      Toast.makeText(appContext, "Speed $speedInKm", Toast.LENGTH_SHORT).show()
    }
  }

  fun showRPM() {
    viewModelScope.launch {
      // Suspend till service gets connected. or at max 1500 ms. which ever comes first.
      val rpm = sensorDataLoggerServiceCoordinator.getService(1500L)?.rpm
      Toast.makeText(appContext, "RMP $rpm", Toast.LENGTH_SHORT).show()
    }
  }

  fun disconnectService() {
    viewModelScope.launch { sensorDataLoggerServiceCoordinator.unbindService() }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Links:&lt;br&gt;
&lt;a href="https://github.com/sridhar-sp/android-playground/blob/main/AIDL/service-connector/src/main/java/io/github/sridhar_sp/service_connector/ServiceConnector.kt" rel="noopener noreferrer"&gt;ServiceConnector&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Repo:&lt;br&gt;
&lt;a href="https://github.com/sridhar-sp/android-playground/tree/main/AIDL" rel="noopener noreferrer"&gt;https://github.com/sridhar-sp/android-playground/tree/main/AIDL&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>ipc</category>
      <category>aidl</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>Simplify Your Android Builds: A Guide to Convention Plugins</title>
      <dc:creator>Sridhar Subramani</dc:creator>
      <pubDate>Sun, 29 Dec 2024 11:30:07 +0000</pubDate>
      <link>https://dev.to/sridharsubramani/simplify-your-android-builds-a-guide-to-convention-plugins-sridhar-subramaniam-4g2n</link>
      <guid>https://dev.to/sridharsubramani/simplify-your-android-builds-a-guide-to-convention-plugins-sridhar-subramaniam-4g2n</guid>
      <description>&lt;h2&gt;
  
  
  Simplify Your Android Builds: A Guide to Convention Plugins
&lt;/h2&gt;

&lt;p&gt;Learn How to Create and Use Custom Gradle Plugins&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F9856%2F0%2AwPbjfdrg2AmS8srm" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F9856%2F0%2AwPbjfdrg2AmS8srm" alt="Photo by [Birger Strahl](https://unsplash.com/@bist31?utm_source=medium&amp;amp;utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral)" width="800" height="450"&gt;&lt;/a&gt;&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@bist31?utm_source=medium&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Birger Strahl&lt;/a&gt; on &lt;a href="https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s a Convention Plugin
&lt;/h2&gt;

&lt;p&gt;A convention plugin is a structured approach to organizing Gradle files, which can be used as a plugin within the Gradle&lt;br&gt;
module.&lt;/p&gt;
&lt;h3&gt;
  
  
  Benefits
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reusable&lt;/strong&gt;: Plugins can be reused across multiple Gradle modules, enhancing consistency and efficiency.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Easy to Maintain&lt;/strong&gt;: Complex build logic can be developed by extending or composing other build logic and can be encapsulated into dedicated plugins (such as &lt;code&gt;ArtifactDeploymentPlugin&lt;/code&gt;, &lt;code&gt;AndroidLibraryPlugin&lt;/code&gt;, and &lt;code&gt;AndroidComposePlugin&lt;/code&gt;, etc.) for ease of use.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Easy to Consume&lt;/strong&gt;: By applying plugins, users can easily add new features to their build process without having to write complex code from scratch again.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Testable Build Logic&lt;/strong&gt;: The build logic can be tested using TestKit to verify its behavior. (Yet to try this)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How to Add This Convention Plugin
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Step 1: Create the &lt;code&gt;build-logic&lt;/code&gt; Folder&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Create a separate &lt;code&gt;build-logic&lt;/code&gt; folder to organize your convention plugin code and separate it from the rest of your project’s code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;At the root level, create a &lt;code&gt;settings.gradle.kts&lt;/code&gt; file with the following content:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;     dependencyResolutionManagement {

          repositories {
              google()
              mavenCentral()
          }

          versionCatalogs {
              create("libs") {
                  from(files("../gradle/libs.versions.toml")) // Create this file if not present.
              }
          }
      }

      rootProject.name = "build-logic"
      // include(":convention") // Enable this line only after step 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Inside the project’s &lt;code&gt;settings.gradle.kts&lt;/code&gt;, add &lt;code&gt;includeBuild(“build-logic”)&lt;/code&gt; within the &lt;code&gt;pluginManagement&lt;/code&gt; block.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      pluginManagement {

          includeBuild("build-logic") // Include the build-logic

          repositories {
              // ...
              gradlePluginPortal()
          }
      }

      // ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Create the Build Convention Module
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Add a new Java/Kotlin module under &lt;code&gt;build-logic&lt;/code&gt; and name it as &lt;code&gt;convention&lt;/code&gt;. In this example, the package name is &lt;code&gt;com.droidstarter.convention&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ensure that any entry like &lt;code&gt;include(“:build-logic:convention”)&lt;/code&gt; in the root settings.gradle.kts file is removed (&lt;em&gt;Studio will add this entry automatically when a new module is created&lt;/em&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Replace the script in the &lt;code&gt;convention&lt;/code&gt; module’s &lt;code&gt;build.gradle.kts&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    plugins {
        `kotlin-dsl`
    }

    group = "com.droidstarter.buildlogic" // Package name for the our plugins

    java {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    dependencies {
        compileOnly(libs.android.gradlePlugin)
        compileOnly(libs.kotlin.gradlePlugin)
    }

    gradlePlugin {
        plugins {
            // Will add in next step
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Add the following dependencies to &lt;code&gt;libs.versions.toml&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    [versions]
    androidGradlePlugin = "8.0.2" # use latest version
    kotlin = "1.9.22" # use latest version

    [libraries]
    android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
    kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;include(“:convention”)&lt;/code&gt; to the &lt;code&gt;settings.gradle.kts&lt;/code&gt; file in the &lt;code&gt;build-logic&lt;/code&gt; module, then perform a Gradle sync.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Create a Custom Gradle Convention Plugin
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;First, set up a basic convention plugin named &lt;code&gt;AndroidApplicationComposeConventionPlugin&lt;/code&gt;. This plugin will centralize&lt;br&gt;
all the build configurations needed for setting up an Android project with Jetpack Compose.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Implement&lt;code&gt;Plugin&amp;lt;Project&amp;gt;&lt;/code&gt; to define and apply custom configurations.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    class AndroidApplicationComposeConventionPlugin : Plugin&amp;lt;Project&amp;gt; {
        override fun apply(target: Project) {
            println("*** AndroidApplicationComposeConventionPlugin invoked ***")
            // Additional configuration here...
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Register the plugin inside the build-logic’s &lt;code&gt;build.gradle.kts&lt;/code&gt;, making it accessible for use in other modules:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    plugins {
        `kotlin-dsl`
    }

    // ...

    gradlePlugin {
        plugins {
            create("androidApplicationCompose") {
                id = "com.droidstarter.convention.application.compose" // This is the id we used to resolve our plugin.
                implementationClass = "com.droidstarter.convention.AndroidApplicationComposeConventionPlugin"
            }
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Apply the newly created plugin to your app module or any Android application module.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    plugins {
        id("com.droidstarter.convention.application.compose")
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Perform a Gradle sync, and you should now see the following logs in the build window:&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;**** AndroidApplicationComposeConventionPlugin invoked ****&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 4: Configure the Plugin for Android App with Jetpack Compose Build Logic
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Apply the Android and Kotlin plugins within your convention plugin so that when it is applied, those plugins are automatically included. This allows you to remove them from the app module’s &lt;code&gt;build.gradle.kts&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    class AndroidApplicationComposeConventionPlugin : Plugin&amp;lt;Project&amp;gt; {
        override fun apply(target: Project) {
            println("*** AndroidApplicationComposeConventionPlugin invoked ***")
            with(target) {
                with(pluginManager) {
                    apply("com.android.application") // Include android application plugin
                    apply("org.jetbrains.kotlin.android") // Ensure project build.gradle declared this plugin
                }
            }
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Configure common settings like &lt;code&gt;compileSdk&lt;/code&gt;, &lt;code&gt;minSdk&lt;/code&gt;, &lt;code&gt;targetSdk&lt;/code&gt;, &lt;code&gt;sourceCompatibility&lt;/code&gt;, &lt;code&gt;targetCompatibility&lt;/code&gt;, and &lt;code&gt;kotlinJvmTarget&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set up Jetpack Compose configuration, including enabling compose features and adding necessary dependencies.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class AndroidApplicationComposeConventionPlugin : Plugin&amp;lt;Project&amp;gt; {
        override fun apply(target: Project) {
            println("*** AndroidApplicationComposeConventionPlugin invoked ***")
            with(target) {
                with(pluginManager) {
                    apply("com.android.application")
                    apply("org.jetbrains.kotlin.android") // Ensure project build.gradle declared this plugin
                }

                extensions.configure&amp;lt;ApplicationExtension&amp;gt; {
                    configureKotlinAndroid(this)
                    defaultConfig.targetSdk = 34
                }
            }
        }
    }


    internal fun Project.configureKotlinAndroid(commonExtension: CommonExtension&amp;lt;*, *, *, *&amp;gt;) {
        val libs = extensions.getByType&amp;lt;VersionCatalogsExtension&amp;gt;().named("libs")

        commonExtension.apply {
            compileSdk = 34

            defaultConfig {
                minSdk = 29
            }

            compileOptions {
                sourceCompatibility = JavaVersion.VERSION_11
                targetCompatibility = JavaVersion.VERSION_11
            }

            buildFeatures {
                compose = true
            }

            composeOptions {
                // Add androidxComposeCompiler in toml
                kotlinCompilerExtensionVersion = libs.findVersion("androidxComposeCompiler").get().toString()
            }

            dependencies {
                // Add androidx-compose-bom in toml
                val bom = libs.findLibrary("androidx-compose-bom").get()
                add("implementation", platform(bom))
                add("androidTestImplementation", platform(bom))
            }
        }

        tasks.withType&amp;lt;KotlinCompile&amp;gt;().configureEach {
            kotlinOptions {
                jvmTarget = JavaVersion.VERSION_11.toString()
            }
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Add the following dependencies to &lt;code&gt;libs.versions.toml&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    [versions]
    androidxComposeCompiler = "1.5.10" # use latest version
    androidxComposeBom = "2024.02.01" # use latest version

    [libraries]
    androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If the steps outlined above feel overwhelming, you can simplify the process by cloning the &lt;a href="https://github.com/sridhar-sp/android-template-repo" rel="noopener noreferrer"&gt;Android template repository available at my GitHub&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/sridhar-sp/android-template-repo" rel="noopener noreferrer"&gt;This&lt;/a&gt; repository already contains the essential build configurations and boilerplate setups, including Hilt, JaCoCo test reporting, and plugins for Android Library and Jetpack Compose, among other features.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




</description>
    </item>
    <item>
      <title>Build a Distributed Task Scheduler Using RabbitMQ and Redis</title>
      <dc:creator>Sridhar Subramani</dc:creator>
      <pubDate>Sun, 29 Dec 2024 11:16:23 +0000</pubDate>
      <link>https://dev.to/sridharsubramani/build-a-distributed-task-scheduler-using-rabbitmq-and-redis-5804</link>
      <guid>https://dev.to/sridharsubramani/build-a-distributed-task-scheduler-using-rabbitmq-and-redis-5804</guid>
      <description>&lt;h2&gt;
  
  
  Build a Distributed Task Scheduler Using RabbitMQ and Redis
&lt;/h2&gt;

&lt;p&gt;Delay Task Execution Using RabbitMQ deadLetterExchange&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frq7kjsq5surnecilgh5k.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frq7kjsq5surnecilgh5k.jpeg" alt="Scheduler" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Are you interested in building a task scheduler using RabbitMQ?&lt;/p&gt;

&lt;p&gt;You may wonder why one should build a task scheduler using RabbitMQ given that it’s a message broker and has no reason to behave as a scheduler.&lt;/p&gt;

&lt;p&gt;Well, a year ago, I was working on a hobby project where I wanted to use RabbitMQ and I happened to have a requirement of executing a piece of code after a certain time. &lt;code&gt;A delayed task execution&lt;/code&gt; we can call it.&lt;/p&gt;

&lt;p&gt;This is where I got curious, wondering if it is possible to delay the message execution. In other words, let’s say a message has been sent to a queue at x time and I want to consume it at x+y time, where y is configurable per message/task.&lt;/p&gt;

&lt;p&gt;I was able to achieve this using &lt;code&gt;deadLetterExchange&lt;/code&gt;. This is a RabbitMQ feature. By using this RabbitMQ feature as core logic, I was able to specify how much time should elapse before the consumer can consume the task or message. &lt;br&gt;
But it’s not a task scheduler yet, it’s simply a delayed task execution. A task scheduler should be able to &lt;code&gt;schedule&lt;/code&gt; and &lt;code&gt;cancel&lt;/code&gt; tasks. Well, this is where Redis comes into play to make this into a task scheduler.&lt;/p&gt;

&lt;p&gt;I hope the above content helps to set the context here. Let’s dive in.&lt;/p&gt;


&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;As part of this post, we are not going to discuss the introduction of RabbitMQ and Redis. It is assumed that you have at least a basic understanding of RabbitMQ and Redis. The sample project which is discussed here is written on NodeJS. Knowledge of NodeJS would be helpful, but it is not required.&lt;/p&gt;


&lt;h2&gt;
  
  
  How to achieve delayed task execution
&lt;/h2&gt;

&lt;p&gt;RabbitMQ will forward the messages to the corresponding queue as soon as they are available. There is no direct way we can tell RabbitMQ to keep the message somewhere and send it to the expected queue after y time. &lt;br&gt;
Let’s see how we can achieve this by using &lt;code&gt;deadLetterExchange&lt;/code&gt; .&lt;/p&gt;
&lt;h3&gt;
  
  
  Dead Letter Exchanges
&lt;/h3&gt;

&lt;p&gt;Messages from a queue can be &lt;code&gt;dead-lettered&lt;/code&gt; (i.e., republished to an exchange) when any of the following events occurs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The message is negatively acknowledged by a consumer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;The message expires due to per-message TTL.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The message was dropped because its queue exceeded a length limit.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are going to use the second option (i.e per-message TTL) to trigger the &lt;code&gt;deadLetterExchange&lt;/code&gt;. Let’s see it in detail.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TTL stands for &lt;strong&gt;T&lt;/strong&gt;ime &lt;strong&gt;T&lt;/strong&gt;o &lt;strong&gt;L&lt;/strong&gt;ive&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let’s &lt;strong&gt;set a TTL for each of our messages&lt;/strong&gt; and send them to a queue. Let’s call it an &lt;code&gt;intermediate queue&lt;/code&gt;. When declaring an &lt;code&gt;intermediate queue&lt;/code&gt;, we set an option that when messages in this queue expire, they should be delivered to another specified exchange. Let’s call it the &lt;code&gt;final exchange&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;message TTL time&lt;/strong&gt; is the time we want to delay. The &lt;code&gt;final exchange&lt;/code&gt; is where our consumers will consume messages from a specified queue.&lt;/p&gt;

&lt;p&gt;So, once the message &lt;code&gt;TTL&lt;/code&gt; expires, our consumer receives the message.&lt;/p&gt;

&lt;p&gt;Now the question is, how do we let the message expire? It’s simple. We are sending the message to the &lt;code&gt;intermediate queue&lt;/code&gt; right. Let’s not allow anyone to consume it from here. If the messages in this &lt;code&gt;intermediate queue&lt;/code&gt; are not consumed till the specified &lt;code&gt;TTL&lt;/code&gt;, then they are &lt;code&gt;dead-lettered&lt;/code&gt; and the message will be delivered to our consumer who is happily consuming messages from the queue of &lt;code&gt;final exchange&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;By setting this &lt;code&gt;TTL&lt;/code&gt; as the delay we require, and sending the message to an &lt;code&gt;intermediate queue&lt;/code&gt; where there are no consumers, we can achieve a &lt;code&gt;delayed task execution&lt;/code&gt; on RabbitMQ.&lt;/p&gt;

&lt;p&gt;Below is the UML diagram to achieve &lt;code&gt;delayed task execution&lt;/code&gt; on RabbitMQ using &lt;code&gt;dead letter exchange&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsoa7jdfr0vouw9obmt4h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsoa7jdfr0vouw9obmt4h.png" alt="RabbitMQ delayed task execution using dead letter exchange" width="583" height="445"&gt;&lt;/a&gt;&lt;em&gt;RabbitMQ delayed task execution using dead letter exchange&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But delayed task execution is not a task scheduler, right. Let’s see how we can build a task scheduler that will provide the option to schedule and cancel tasks using RabbitMQ and Redis.&lt;/p&gt;


&lt;h2&gt;
  
  
  Task Scheduler
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The Role of Redis
&lt;/h3&gt;

&lt;p&gt;Redis is used to track the validity of the scheduled task. Using this, we can set any scheduled task as invalid if required, and when it’s time to execute the task, we can check whether the task is valid to execute or not.&lt;/p&gt;

&lt;p&gt;Since we would be using Redis to only store task validity information, it can be replaced with any data storage tool.&lt;/p&gt;
&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F29mzkyq3zmeh6kj24g7y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F29mzkyq3zmeh6kj24g7y.png" alt="task scheduler architecture" width="800" height="611"&gt;&lt;/a&gt;&lt;em&gt;task scheduler architecture&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The list entities we’d use to build this task scheduler are as follows.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/sridhar-sp/task-scheduler/blob/main/src/task/task.ts" rel="noopener noreferrer"&gt;Task&lt;/a&gt; → Task definition.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/sridhar-sp/task-scheduler/blob/main/src/rabbitmq/producer.ts" rel="noopener noreferrer"&gt;Producer&lt;/a&gt; → Base class provides option to send delayed message.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/sridhar-sp/task-scheduler/blob/main/src/rabbitmq/consumer.ts" rel="noopener noreferrer"&gt;Consumer&lt;/a&gt; → Base class to consume messages from final queue.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/sridhar-sp/task-scheduler/blob/main/src/task/repository/taskRepositoryImpl.ts" rel="noopener noreferrer"&gt;Task Repository&lt;/a&gt; → Repository class to check the task validity status.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/sridhar-sp/task-scheduler/blob/main/src/task/scheduler/taskSchedulerImpl.ts" rel="noopener noreferrer"&gt;Task Scheduler&lt;/a&gt; → Class provides the option to schedule and invalidate tasks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/sridhar-sp/task-scheduler/blob/main/src/task/consumer/taskConsumerImpl.ts" rel="noopener noreferrer"&gt;Task Consumer&lt;/a&gt; → Class to consumes tasks from specified task type queue.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Task
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    class Task {
      public taskId: string;
      public taskType: string; // queue name
      public ttlInSeconds: number;
      public payload: string;
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;taskId&lt;/code&gt; → Unique id to identity this task.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;taskType&lt;/code&gt; → Type of this task. this is used as a queue name so both producer and consumer can infer name the from the task type.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;ttlInSeconds&lt;/code&gt; → The time delay after which we want this task to get executed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;payload&lt;/code&gt; → A JSON string describing the task; The &lt;code&gt;taskType&lt;/code&gt; will come in handy to parse this json as per the different task types.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;taskType&lt;/code&gt; plays an important role. It acts as a queue name, so when a task scheduler wants to schedule a task, it can use taskType as a queue name, and whoever wants to consume the particular task type can consume it from that queue.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Following are some of the examples of taskType &lt;code&gt;send-sms&lt;/code&gt;, &lt;code&gt;send-offer-notification&lt;/code&gt;, &lt;code&gt;send-greetings&lt;/code&gt; etc&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Producer
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    public sendDelayedMessageToQueue(taskType: string, delayInMills: number, data: string): Promise&amp;lt;void&amp;gt; {
        const INTERMEDIATE_QUEUE = `${taskType}_INTERMEDIATE_QUEUE`;
        const INTERMEDIATE_EXCHANGE = `${taskType}_INTERMEDIATE_EXCHANGE`;
        const INTERMEDIATE_EXCHANGE_TYPE = "fanout";

        const FINAL_QUEUE = taskType;
        const FINAL_EXCHANGE = `${taskType}_FINAL_EXCHANGE`;
        const FINAL_EXCHANGE_TYPE = "fanout";

        return new Promise((resolve, reject) =&amp;gt; {
          this.assertExchange(INTERMEDIATE_EXCHANGE, INTERMEDIATE_EXCHANGE_TYPE)
            .then((_) =&amp;gt; this.assertExchange(FINAL_EXCHANGE, FINAL_EXCHANGE_TYPE))
            .then((_) =&amp;gt; this.assertQueue(INTERMEDIATE_QUEUE, { deadLetterExchange: FINAL_EXCHANGE }))
            .then((_) =&amp;gt; this.assertQueue(FINAL_QUEUE, {}))
            .then((_) =&amp;gt; this.bindQueue(INTERMEDIATE_QUEUE, INTERMEDIATE_EXCHANGE, ""))
            .then((_) =&amp;gt; this.bindQueue(FINAL_QUEUE, FINAL_EXCHANGE, ""))
            .then((_) =&amp;gt; {
              this.channel?.sendToQueue(INTERMEDIATE_QUEUE, Buffer.from(data), {
                expiration: delayInMills, // The delay after which we want to send this message
              });
              resolve();
            })
            .catch(reject);
        });
      }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;As we discussed earlier, we need two queues to achieve this delayed task &lt;code&gt;execution&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Intermediate queue → Holds the task till expiration; post expiration forwards the task to &lt;code&gt;final exchange&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Final queue → This is associated with the &lt;code&gt;final exchange&lt;/code&gt;. Our task consumer will listen to this queue.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both the queue and its associated exchange names are formed from &lt;code&gt;taskType&lt;/code&gt; to bring in some sort of contract between scheduler and consumer.&lt;/p&gt;

&lt;p&gt;From the above code, we can see the intermediate queue is associated with the final exchange as a &lt;code&gt;dead letter exchange&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    this.assertQueue(
        INTERMEDIATE_QUEUE, { deadLetterExchange: FINAL_EXCHANGE }
    )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the task expiration time has elapsed, the task will be forwarded to the &lt;code&gt;final exchange&lt;/code&gt;, and the &lt;code&gt;final exchange&lt;/code&gt; will route the task to the final queue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    sendToQueue(INTERMEDIATE_QUEUE, Buffer.from(data), {
        expiration: delayInMills,
    });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Consumer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      public consume(taskType: string, handler: (payload: string) =&amp;gt; void) {
        const FINAL_QUEUE = taskType;
        const FINAL_EXCHANGE = `${taskType}_FINAL_EXCHANGE`;
        const FINAL_EXCHANGE_TYPE = "fanout";

        this.assertExchange(FINAL_EXCHANGE, FINAL_EXCHANGE_TYPE)
          .then((_) =&amp;gt; this.assertQueue(FINAL_QUEUE, {}))
          .then((_) =&amp;gt; this.bindQueue(FINAL_QUEUE, FINAL_EXCHANGE, ""))
          .then((_) =&amp;gt; {
            this.channel?.consume(
              FINAL_QUEUE,
              (msg: Message | null) =&amp;gt; {
                handler(msg?.content ? msg?.content.toString() : "");
              },
              { noAck: true }
            );
          });
      }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A consumer will consume messages from a specific queue (i.e. &lt;code&gt;taskType&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Task Repository
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    interface TaskRepository {
      createTask(taskId: string, ttlInSeconds: number): Promise&amp;lt;void&amp;gt;;
      deleteTask(taskId: string): Promise&amp;lt;void&amp;gt;;
      isTaskValid(taskId: string): Promise&amp;lt;boolean&amp;gt;;
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides options to create, delete, and validate tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Task Scheduler
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    scheduleTask(delayInMilliseconds: number, task: Task): Promise&amp;lt;string&amp;gt; {
       return new Promise((resolve: (taskId: string) =&amp;gt; void, reject: (error: Error) =&amp;gt; void) =&amp;gt; {
         this.taskRepository
           .createTask(task.taskId, task.ttlInSeconds)
           .then(() =&amp;gt; this.producer.sendDelayedMessageToQueue(task.taskType, delayInMilliseconds, task.toJson()))
           .then(() =&amp;gt; resolve(task.taskId))
           .catch((error) =&amp;gt; reject(error));
       });
    }


    invalidateTask(taskId: string): Promise&amp;lt;void&amp;gt; {
        return this.taskRepository.deleteTask(taskId);
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Task Scheduler&lt;/code&gt; will create a task entry in Redis when scheduling a task. &lt;br&gt;
When we no longer want the task to be executed, the task can be deleted to mark it as invalid. The &lt;code&gt;scheduleTask&lt;/code&gt; returns a &lt;code&gt;taskId&lt;/code&gt;. The same can be used to invalidate the task if required.&lt;/p&gt;
&lt;h3&gt;
  
  
  Task Consumer
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    consume(taskType: string, handler: (task: Task) =&amp;gt; void): void {
        this.consumer.consume(taskType.toString(), async (payload: string) =&amp;gt; {
          const task = Task.fromJson(payload);
          const isTaskValid = await this.taskRepository.isTaskValid(task.taskId);

          if (isTaskValid) handler(task);
        });
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;Task Consumer&lt;/code&gt; will listen for a specific &lt;code&gt;taskType&lt;/code&gt; and execute it if the task is still valid.&lt;br&gt;
Even after we invalidate the task by deleting it. It will be consumed by the consumer at the scheduled time. It will be the &lt;code&gt;Task Consumer&lt;/code&gt; responsibility to check the task validity before executing the task using &lt;code&gt;isTaskValid&lt;/code&gt; function from the &lt;code&gt;Task Repository&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The demo includes two consumers and two producers. &lt;br&gt;
Consider consumer 1 and 2 as email services, each microservice responsible for sending &lt;code&gt;greetings&lt;/code&gt; and &lt;code&gt;offer notification&lt;/code&gt; to customers. &lt;br&gt;
The greeting and offer-notification are two different &lt;code&gt;task types&lt;/code&gt; which will be produced by &lt;code&gt;Greeter-Service&lt;/code&gt; and &lt;code&gt;Offer-Notification-Service&lt;/code&gt; producers respectively.&lt;/p&gt;

&lt;p&gt;The demo shows that &lt;code&gt;Email-Service-1&lt;/code&gt; (i.e., consumer 1) will process both greet and &lt;code&gt;offer-notification&lt;/code&gt; tasks, where as &lt;code&gt;Email-Service-2&lt;/code&gt; (i.e., consumer 2) will process only &lt;code&gt;offer-notification tasks&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Due to brevity, the task invalidation is not shown on the demo but is explained in the project &lt;a href="https://github.com/sridhar-sp/task-scheduler#invalidate-scheduled-task" rel="noopener noreferrer"&gt;Readme&lt;/a&gt; file.&lt;/p&gt;

&lt;p&gt;I tried my best to animate and highlight log messages to indicate the event flow between &lt;code&gt;scheduler&lt;/code&gt; and &lt;code&gt;consumer&lt;/code&gt;. The log message includes a timestamp, which can be used to assert whether the consumer is getting a task notification at the expected scheduled time or not. I have highlighted the timestamp on both the &lt;code&gt;scheduler&lt;/code&gt; and the &lt;code&gt;consumer&lt;/code&gt; whenever the &lt;code&gt;consumer&lt;/code&gt; receives a &lt;code&gt;task&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/OQUoMo34Zgk"&gt;
&lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  Source Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/sridhar-sp/task-scheduler" rel="noopener noreferrer"&gt;https://github.com/sridhar-sp/task-scheduler&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Reference
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.rabbitmq.com/dlx.html" rel="noopener noreferrer"&gt;https://www.rabbitmq.com/dlx.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/sridhar-sp/draw-server" rel="noopener noreferrer"&gt;https://github.com/sridhar-sp/draw-server&lt;/a&gt; (Multiplayer drawing game using this task-scheduler)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://play.google.com/store/apps/details?id=com.gandiva.draw" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.gandiva.draw&lt;/a&gt; (Production app using this task-scheduler)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>eventdriven</category>
      <category>distributedsystems</category>
      <category>javascript</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Build a Tic-Tac-Toe Game In the GitHub README.md File</title>
      <dc:creator>Sridhar Subramani</dc:creator>
      <pubDate>Sun, 29 Dec 2024 10:12:01 +0000</pubDate>
      <link>https://dev.to/sridharsubramani/build-a-tic-tac-toe-game-in-the-github-readmemd-file-490m</link>
      <guid>https://dev.to/sridharsubramani/build-a-tic-tac-toe-game-in-the-github-readmemd-file-490m</guid>
      <description>&lt;h3&gt;
  
  
  Pushing the boundaries of the markdown file using Go
&lt;/h3&gt;

&lt;p&gt;What would be your initial response when someone asked “is it possible to play a tic-tac-toe game from Github README.md file” ?&lt;/p&gt;

&lt;p&gt;If your answer is “yes”, then you are in for a delight.&lt;/p&gt;

&lt;p&gt;Let’s dive in and see how it’s possible to make a Github README.md file as a frontend for a tic-tac-toe game.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It All Started
&lt;/h2&gt;

&lt;p&gt;You may ask why I even went on this journey, and the following are a few of the reasons behind it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;I wanted to push the boundaries of a markdown file and make it as interactive as possible.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;to uniquely identify and track the traffic coming to my individual GitHub project page.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why did I even get these wild ideas in the first place? Well, it’s because whenever I add an image or image link to the GitHub readme markdown file, it’s always getting replaced with a proxy URL when I try to open the image(proxy URL looks like this: &lt;a href="https://camo.githubusercontent.com/some-hash%E2%80%99" rel="noopener noreferrer"&gt;&lt;code&gt;https://camo.githubusercontent.com/some-hash&lt;/code&gt;&lt;/a&gt; ).&lt;/p&gt;

&lt;p&gt;I wondered why it was happening like that. I mean, it’s okay to proxy an external URL, but why are even static images placed in repositories getting a proxy URL when they’re referenced from a readme file? What is GitHub trying to achieve here?&lt;/p&gt;

&lt;p&gt;Then I thought about what would happen if there was no proxy server involved while serving the images. If there is no proxy, then the HTTP request to fetch the image will directly come to our server(where we host the image) and we can read the HTTP request and try to get a user IP address, which can be used for any tracking purposes and GitHub will have no control over it.&lt;/p&gt;

&lt;p&gt;So I kind of understood the reason behind why Github added the image proxy server. At this stage, I wanted to know whether it’s possible to get any unique identification from the HTTP request made from the user machine as part of rendering the markdown file.&lt;/p&gt;

&lt;p&gt;I hoped there would be a way to uniquely identify each session/machine. So I thought of building a simple game that can be played from within the markdown file itself to test that. The idea was to build a multiplayer game where each player would have their own state saved against a unique id. “Tic-tac-toe” game seems to be a good fit for this, since it’s a well-known game, and it’s fairly straightforward to implement.&lt;/p&gt;

&lt;p&gt;After I started the development, I quickly found out there was no way we could get a unique id from the http request fired from the readme markdown file. This is because each time a request comes from a random proxy server, and on top of that, cookies are not allowed either.&lt;/p&gt;

&lt;p&gt;So, without a unique id, there won’t be any states saved for each player, and individual gameplay is impossible. Therefore, only a single gameplay will be shared with the entire internet. (In other words, anyone in the world can see and play the game, but only one gameplay is visible to everyone; changes made by one are reflected to all.)&lt;/p&gt;

&lt;p&gt;At this stage, I thought of abandoning the quest, but playing a game from the Github README.md file itself seems like a cool idea, even when the gameplay is shared with the entire internet. So I ran this idea by my friends, and they seemed interested in seeing how this would turn out, so I thought of investing some time in developing this.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;GitHub uses a proxy server to serve media content, and each time a random proxy server will be used to serve media (when the cache is disabled).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The proxy server doesn’t allow us to store cookies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And we would like to make markdown interactive with a dynamic UI.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Life Cycle of an HTTP Request From a GitHub Markdown File
&lt;/h2&gt;

&lt;p&gt;Let’s consider the below Github project structure — where we have &lt;code&gt;localImageFile.png&lt;/code&gt; and &lt;code&gt;README.md&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;README.md&lt;/code&gt; file shows two images, one from the repository itself and the other from a remote server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy2nex0gm21pts36h7ke0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy2nex0gm21pts36h7ke0.png" alt="Github project structure" width="800" height="744"&gt;&lt;/a&gt;&lt;em&gt;Github project structure&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The below sequence diagram is illustrated for loading localImageFile.png from the same repo:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F90zw0blg26caiabxhxza.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F90zw0blg26caiabxhxza.png" alt="Local image loading sequence" width="800" height="553"&gt;&lt;/a&gt;&lt;em&gt;Local image loading sequence&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The below sequence diagram is illustrated for loading remoteImage.png from the remote server:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi9znl0h94f2j9nt8g4vp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi9znl0h94f2j9nt8g4vp.png" alt="Remote image loading sequence" width="800" height="468"&gt;&lt;/a&gt;&lt;em&gt;Remote image loading sequence&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Let’s Build the Tic-Tac-Toe
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Building APIs for the game — We are going to build a backend server that exposes APIs to &lt;code&gt;start&lt;/code&gt; the game, &lt;code&gt;restart&lt;/code&gt; the game, &lt;code&gt;read the current state&lt;/code&gt; of the game, &lt;code&gt;make a move&lt;/code&gt; from the user, and &lt;code&gt;read the history of the game state&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How do we call these APIs from a Github Readme Markdown file — Well, we are going to create CTAs (call to action) for each of the APIs and call the APIs whenever the CTAs are clicked.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How do we create CTAs on Markdown files — For CTAs, we will use images, and when the image is clicked, an HTTP request will be fired from the markdown file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How do we show the tic-tac-toe board on UI — You might have already guessed it. We will use images to reflect the current state of the game.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, the backend will return a set of images we can use as CTA, and when clicking the images, we fire the corresponding API which will manipulate the game state.&lt;/p&gt;

&lt;p&gt;As I mentioned earlier, since we can’t identify the request made from the same system, only one gameplay will be shared with the entire internet. Hence, the backend maintains the game state of the 3x3 tic-tac-toe in a single variable. The APIs that are exposed will basically try to modify this single game state and reflect the same on the UI (i.e. the README file) using images sent from the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Image assets stored on the backend server
&lt;/h3&gt;

&lt;p&gt;Please keep the svg file names in mind as we will go over the uses for each image file shortly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2186%2F1%2AhX5OOG7tjNFgXls1ZwiAkw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2186%2F1%2AhX5OOG7tjNFgXls1ZwiAkw.png" alt="[Image assets stored on the backend server](https://github.com/sridhar-sp/tic-tac-toe-backend/tree/main/assets)" width="800" height="600"&gt;&lt;/a&gt;&lt;em&gt;&lt;a href="https://github.com/sridhar-sp/tic-tac-toe-backend/tree/main/assets" rel="noopener noreferrer"&gt;Image assets stored on the backend server&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The backend server is written in Go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func main() {
    port := getPort()
    log.Println("Starting tic-tac-toe service at port ", port)

    http.Handle("/", http.HandlerFunc(onHome))
    http.Handle("/renderCell", http.HandlerFunc(onRenderCell))
    http.Handle("/clickCell", http.HandlerFunc(onClickCell))
    http.Handle("/renderPlayControls", http.HandlerFunc(onRenderPlayControl))
    http.Handle("/clickPlayControls", http.HandlerFunc(onPlayControlClick))
    http.Handle("/renderActivities", http.HandlerFunc(renderActivities))

    http.ListenAndServe(":"+port, nil)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s look at each API and see what it does!&lt;/p&gt;

&lt;h3&gt;
  
  
  Render cell
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The &lt;a href="https://tic-tac-toe-backend.onrender.com/renderCell?cellIndex=0" rel="noopener noreferrer"&gt;&lt;code&gt;renderCell&lt;/code&gt;&lt;/a&gt; API takes &lt;em&gt;cell index&lt;/em&gt; as a URL parameter and returns the cell image. For a 3x3 tic-tac-toe game, we have nine cells to render. The backend returns the corresponding cell image based on the cell’s state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;rect_x.svg&lt;/code&gt; is returned when the user has pressed that cell.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;rect_o.svg&lt;/code&gt; is returned when the computer has pressed that cell.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;rect_empty.svg&lt;/code&gt; is returned when no one has pressed that cell.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;rect_x_selected.svg&lt;/code&gt; is returned to highlight the area won by the user.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;rect_o_selected.svg&lt;/code&gt; is returned to highlight the area won by the computer.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Click cell
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The &lt;a href="https://tic-tac-toe-backend.onrender.com/clickCell?cellIndex=0" rel="noopener noreferrer"&gt;&lt;code&gt;clickCell&lt;/code&gt; API&lt;/a&gt; takes the cell index as a URL parameter and marks the cell as selected by the user (provided the cell is empty).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;clickCell&lt;/code&gt; API is attached to the anchor link of each cell that is rendered by the renderCell API.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Please see the screenshot below to see the markdown file content and its preview:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2232%2F1%2AJS2OxuqpIyGxBxD7A6LyVg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2232%2F1%2AJS2OxuqpIyGxBxD7A6LyVg.png" alt="[Markdown file content with a preview](https://github.com/sridhar-sp/tic-tac-toe)" width="800" height="308"&gt;&lt;/a&gt;&lt;em&gt;&lt;a href="https://github.com/sridhar-sp/tic-tac-toe" rel="noopener noreferrer"&gt;Markdown file content with a preview&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;When each cell is clicked, the server modifies the game state and reloads the page so that the new modified state can be read from the server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Page reloading is a mandatory step. Since this is a markdown file, this is the only way to get the modified state from the server.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reload the page
&lt;/h3&gt;

&lt;p&gt;This is one of the crucial steps. Remember how GitHub uses an image proxy server to serve media files? Well, that image proxy caches the response for a certain amount of time. So we need to add the cache control response header as ‘no-cache,no-store,must-revalidate’ to disable this cache mechanism.&lt;/p&gt;

&lt;p&gt;If we don’t disable the cache mechanism, then the proxy server will simply return the cached response and will not call our backend till the cache expires. Therefore, we won’t be able to reflect the current game state back to the user, and we will end up showing stale data (not a good user experience).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    const REDIRECT_URL = "https://github.com/sridhar-sp/tic-tac-toe"

    responseWriter.Header().Set("Content-Type", "image/svg+xml") 

    responseWriter.Header().Set("Cache-Control", "no-cache,
    no-store,must-revalidate")

    responseWriter.Header().Set("expires", "0") 
    responseWriter.Header().Set("pragma", "no-cache")

    http.Redirect(
     responseWriter, req, 
     REDIRECT_URL, http.StatusMovedPermanently
    )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Render play controls
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://tic-tac-toe-backend.onrender.com/renderPlayControls" rel="noopener noreferrer"&gt;&lt;code&gt;renderPlayControls&lt;/code&gt;&lt;/a&gt; API returns computer_start_button.svg when the game is yet to begin and restart_button.svg when the game is finished.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Click play controls
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;This &lt;a href="https://tic-tac-toe-backend.onrender.com/clickPlayControls" rel="noopener noreferrer"&gt;API&lt;/a&gt; is attached as an anchor to renderPlayControls&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When the play control button is pressed, the corresponding action takes place on the backend, such as restarting the game or letting the computer make the first move.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Please see the screenshot below to see the play control markdown file content and its preview:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9mdihh41t7w7u32qqr1a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9mdihh41t7w7u32qqr1a.png" alt="Play controls preview" width="800" height="245"&gt;&lt;/a&gt;&lt;em&gt;Play controls preview&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Render game activities
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://tic-tac-toe-backend.onrender.com/renderActivities" rel="noopener noreferrer"&gt;&lt;code&gt;renderActivities&lt;/code&gt;&lt;/a&gt; API returns all game activities in svg image format.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Please see the screenshot below to see the game activities markdown file content and its preview:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgst934bnj5py4ngt5spl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgst934bnj5py4ngt5spl.png" alt="Game activities preview" width="800" height="308"&gt;&lt;/a&gt;&lt;em&gt;Game activities preview&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Player ‘X’ is a human, and Player ‘O’ is a computer.&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7o9lefskgxypvujehwnk.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7o9lefskgxypvujehwnk.gif" alt="Demo" width="600" height="749"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Please see the screenshot below to see the markdown file content and its preview:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1r8kn0t0j6fsd2sz28hf.gif" rel="noopener noreferrer"&gt;Click here to see Markdown file with its interactive preview&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Source Code
&lt;/h2&gt;

&lt;p&gt;Here are the GitHub repositories for the final frontend application and the backend written in Go:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/sridhar-sp/tic-tac-toe" rel="noopener noreferrer"&gt;Frontend — Readme.md&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/sridhar-sp/tic-tac-toe-backend" rel="noopener noreferrer"&gt;Backend in Go&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Reference
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/atmos/camo" rel="noopener noreferrer"&gt;https://github.com/atmos/camo&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.blog/2010-11-13-sidejack-prevention-phase-3-ssl-proxied-assets/" rel="noopener noreferrer"&gt;https://github.blog/2010-11-13-sidejack-prevention-phase-3-ssl-proxied-assets/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-anonymized-urls" rel="noopener noreferrer"&gt;https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-anonymized-urls&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Library used
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ajstarks/svgo" rel="noopener noreferrer"&gt;https://github.com/ajstarks/svgo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Build Android App Widgets With Jetpack Glance</title>
      <dc:creator>Sridhar Subramani</dc:creator>
      <pubDate>Sun, 29 Dec 2024 09:43:41 +0000</pubDate>
      <link>https://dev.to/sridharsubramani/build-android-app-widgets-with-jetpack-glance-136n</link>
      <guid>https://dev.to/sridharsubramani/build-android-app-widgets-with-jetpack-glance-136n</guid>
      <description>&lt;h4&gt;
  
  
  A modern approach to building android app widgets.
&lt;/h4&gt;

&lt;p&gt;Before diving into the topic, let’s quickly take a history lesson to see why this &lt;a href="https://developer.android.com/jetpack/androidx/releases/glance" rel="noopener noreferrer"&gt;Glance&lt;/a&gt; is an important and long-awaited feature.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Android app widgets were released as part of Android 1.5 (Cupcake).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://cs.android.com/android/_/android/platform/frameworks/base/+/c39a6e0c51e182338deb8b63d07933b585134929:core/java/android/appwidget/AppWidgetManager.java;l=64;bpv=1;bpt=0" rel="noopener noreferrer"&gt;Code inception&lt;/a&gt; in 2009.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In 2012 support added to display widgets on the lock screen, and provided third party apps to act as widget host (custom launcher application) and etc.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Over the period of time android system grew and received a lot of updates on the other part of the system but got very few updates over the widget framework.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Android 12 introduces &lt;a href="https://developer.android.com/jetpack/androidx/releases/glance" rel="noopener noreferrer"&gt;Glance&lt;/a&gt; framework to create app widgets using jetpack-compose (declarative style) and much more additional updates to the widget framework itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What’s new in Android 12&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://developer.android.com/jetpack/androidx/releases/glance" rel="noopener noreferrer"&gt;&lt;strong&gt;Jetpack Glance&lt;/strong&gt;&lt;/a&gt; for app widgets
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Declarative API to build app widget UI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stateful widgets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;New clean api to handle user interaction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Custom error UI (from XML).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Other updates on the widget framework&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Scalable widget preview&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Better theming support&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;New compound buttons&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;New API’s added to allow runtime modification of RemoteViews&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Jetpack Glance for app widgets&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What is Jetpack Glance&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Jetpack Glance is a new framework built on top of the Jetpack Compose runtime.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Glance offers similar modern declarative Kotlin APIs that come with Jetpack Compose which helps to build responsive app widgets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As it builds on top of Jetpack Compose runtime it’s not directly interoperable with other existing Jetpack Compose UI elements but interoperable with existing RemoteViews.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Glance provides a base set of &lt;a href="https://developer.android.com/jetpack/compose/tutorial" rel="noopener noreferrer"&gt;composables&lt;/a&gt; to help build “glanceable” experiences.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Using the Jetpack Compose runtime Glance can translate &lt;a href="https://developer.android.com/jetpack/compose/tutorial" rel="noopener noreferrer"&gt;composables&lt;/a&gt; into actual RemoteViews and display them in an app widget.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Glance provides a more intuitive API to handle user interactions. It abstracts away the complexities we would encounter while using RemoteViews and PendingIntent. It provides the following predefined actions for handling user interactions.&lt;br&gt;
1) &lt;a href="https://developer.android.com/reference/kotlin/androidx/glance/appwidget/action/package-summary#actionruncallback" rel="noopener noreferrer"&gt;actionRunCallback&lt;/a&gt; &lt;br&gt;
2) &lt;a href="https://developer.android.com/reference/kotlin/androidx/glance/appwidget/action/package-summary#actionstartactivity" rel="noopener noreferrer"&gt;actionStartActivity&lt;/a&gt;&lt;br&gt;
3) &lt;a href="https://developer.android.com/reference/kotlin/androidx/glance/appwidget/action/package-summary#actionstartservice" rel="noopener noreferrer"&gt;actionStartService&lt;/a&gt;&lt;br&gt;
4) &lt;a href="https://developer.android.com/reference/kotlin/androidx/glance/appwidget/action/package-summary#actionsendbroadcast" rel="noopener noreferrer"&gt;actionSendBroadcast&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inbuilt support for different size UI by defining &lt;a href="https://developer.android.com/reference/kotlin/androidx/glance/appwidget/SizeMode.Single" rel="noopener noreferrer"&gt;SizeMode.Single,&lt;/a&gt; &lt;a href="https://developer.android.com/reference/kotlin/androidx/glance/appwidget/SizeMode.Exact" rel="noopener noreferrer"&gt;SizeMode.Exact&lt;/a&gt; or &lt;a href="https://developer.android.com/reference/kotlin/androidx/glance/appwidget/SizeMode.Responsive" rel="noopener noreferrer"&gt;SizeMode.Responsive&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;State management using &lt;a href="https://developer.android.com/reference/kotlin/androidx/glance/state/GlanceStateDefinition.html" rel="noopener noreferrer"&gt;GlanceStateDefinition&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;LocalContext&lt;/code&gt;, &lt;code&gt;LocalState&lt;/code&gt;, &lt;code&gt;LocalGlanceId&lt;/code&gt;, &lt;code&gt;LocalSize&lt;/code&gt;. &lt;code&gt;LocalAppWidgetOptions&lt;/code&gt; are provided to the &lt;code&gt;content()&lt;/code&gt; method using &lt;code&gt;CompositionLocalProvider&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Even though the Glance framework seems like a fundamental change to the app widget. The creation of the app widget is pretty much the same and the only change is in how we represent the UI and maintain the state.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Create GlanceAppWidget&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Step 0: &lt;a href="https://developer.android.com/jetpack/androidx/releases/glance#declaring_dependencies" rel="noopener noreferrer"&gt;Add dependency&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    dependencies {
        // For AppWidgets support
        implementation "androidx.glance:glance-appwidget:1.0.0-alpha04"

        // For Wear-Tiles support
        implementation "androidx.glance:glance-wear-tiles:1.0.0-alpha04"
    }

    android {
        buildFeatures {
            compose true
        }

        composeOptions {
            kotlinCompilerExtensionVersion = "1.1.0-beta03"
        }

        kotlinOptions {
            jvmTarget = "1.8"
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 1: Create &lt;code&gt;GlanceAppWidget&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    package com.gandiva.glance.resizeable.widget

    import androidx.compose.runtime.Composable
    import androidx.glance.appwidget.GlanceAppWidget
    import androidx.glance.text.Text

    class SimpleGlanceAppWidget : GlanceAppWidget() {

        @Composable
        override fun Content() {
            Text(text = "Hello!")
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 2: Attach with &lt;a href="https://developer.android.com/reference/androidx/glance/appwidget/GlanceAppWidgetReceiver" rel="noopener noreferrer"&gt;GlanceAppWidgetReceiver&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    package com.gandiva.glance.resizeable.widget

    import androidx.glance.appwidget.GlanceAppWidget
    import androidx.glance.appwidget.GlanceAppWidgetReceiver

    class SimpleGlanceAppWidgetReceiver : GlanceAppWidgetReceiver() {
        override val glanceAppWidget: GlanceAppWidget
            get() = SimpleGlanceAppWidget()
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 3: Add widget &lt;a href="https://developer.android.com/reference/android/appwidget/AppWidgetProviderInfo" rel="noopener noreferrer"&gt;meta-data&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    &amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;
    &amp;lt;appwidget-provider   xmlns:android="http://schemas.android.com/apk/res/android"
        android:description="@string/app_widget_description"
        android:initialLayout="@layout/simple_widget"
        android:previewLayout="@layout/simple_widget"
        android:minWidth="250dp"
        android:minHeight="110dp"
        android:resizeMode="horizontal|vertical"
        android:widgetCategory="home_screen"
        android:previewImage="@drawable/appwidget_preview"
        android:maxResizeWidth="1050dp"
        android:maxResizeHeight="787dp"
        android:targetCellWidth="5"
        android:targetCellHeight="3"
        /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Final step: Register component in AndroidManifest.xml&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    &amp;lt;receiver
        android:name=".resizeable.widget.SimpleGlanceAppWidgetReceiver"
        android:exported="true"&amp;gt;
        &amp;lt;intent-filter&amp;gt;
            &amp;lt;action android:name="android.appwidget.action.APPWIDGET_UPDATE" /&amp;gt;
        &amp;lt;/intent-filter&amp;gt;

        &amp;lt;meta-data
            android:name="android.appwidget.provider"
            android:resource="@xml/simple_glance_widget_info" /&amp;gt;
    &amp;lt;/receiver&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  &lt;strong&gt;Stateful widget&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Each &lt;code&gt;GlanceAppWidget&lt;/code&gt; maintains its own preference to store data as key-value pair using &lt;a href="https://developer.android.com/topic/libraries/architecture/datastore" rel="noopener noreferrer"&gt;Datastore&lt;/a&gt; preference.&lt;/p&gt;

&lt;p&gt;To read the widget state use &lt;code&gt;currentState&lt;/code&gt; method from local composition (i.e &lt;code&gt;LocalState.current&lt;/code&gt;). &lt;code&gt;currentState&lt;/code&gt; has two overloaded methods:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;currentState&lt;/code&gt; with no argument returns a &lt;a href="https://developer.android.com/reference/kotlin/androidx/datastore/preferences/core/Preferences" rel="noopener noreferrer"&gt;Preferences&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;currentState&lt;/code&gt; with key as argument(i.e &lt;code&gt;currentState(key)&lt;/code&gt;) returns a value associated with the key.&lt;/p&gt;

&lt;p&gt;To update data into the preference we can use &lt;a href="https://developer.android.com/reference/kotlin/androidx/glance/appwidget/state/package-summary#updateappwidgetstate" rel="noopener noreferrer"&gt;&lt;code&gt;updateAppWidgetState()&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Once the preference is updated using &lt;a href="https://developer.android.com/reference/kotlin/androidx/glance/appwidget/state/package-summary#updateappwidgetstate" rel="noopener noreferrer"&gt;&lt;code&gt;updateAppWidgetState()&lt;/code&gt;&lt;/a&gt; we have to manually call &lt;code&gt;GlanceAppWidget.update()&lt;/code&gt; method to recompose the UI with latest data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let’s look at the below code to see how we are updating the boolean preference value to toggle the widget theme.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;Stateful widget code&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F87edkfblhxfukqu8iu4y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F87edkfblhxfukqu8iu4y.png" alt="Stateful widget code" width="800" height="1060"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;Stateful widget demo&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4a4ocy9y20ntxlsaz5se.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4a4ocy9y20ntxlsaz5se.gif" alt="Stateful widget demo" width="480" height="1013"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;User interaction&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;As we discussed earlier Glance provides more intuitive APIs to handle user interaction and &lt;code&gt;actionRunCallback&lt;/code&gt; is one of them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;actionRunCallback&lt;/code&gt; method returns an Action that can be attached to the onClick method of a button or any other Glance component with &lt;code&gt;onClick&lt;/code&gt; attribute.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;actionRunCallback&lt;/code&gt; method takes &lt;code&gt;ActionCallback&lt;/code&gt; class as a type parameter and optional &lt;code&gt;ActionParameters&lt;/code&gt; as method argument.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s look at the below code whenever the button is clicked an instance of &lt;code&gt;ToastActionCallback&lt;/code&gt; will be created and &lt;code&gt;onRun&lt;/code&gt; method will be called on that with &lt;code&gt;context&lt;/code&gt;, &lt;code&gt;glanceId&lt;/code&gt; and the parameters we passed (i.e &lt;code&gt;ActionParameters&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;PS: &lt;code&gt;glanceId&lt;/code&gt; is a unique id assigned to each GlanceAppWidget.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0j39wchkserleztnhe8g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0j39wchkserleztnhe8g.png" alt="Handler user interaction with actionRunCallback (with action parameters)" width="800" height="556"&gt;&lt;/a&gt;&lt;em&gt;Handler user interaction with actionRunCallback (with action parameters)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Just like &lt;code&gt;actionRunCallback&lt;/code&gt; we have other API to start a service (i.e &lt;a href="https://developer.android.com/reference/kotlin/androidx/glance/appwidget/action/package-summary#actionstartservice" rel="noopener noreferrer"&gt;&lt;code&gt;actionStartService&lt;/code&gt;&lt;/a&gt;), start an activity (i.e &lt;a href="https://developer.android.com/reference/kotlin/androidx/glance/appwidget/action/package-summary#actionstartactivity" rel="noopener noreferrer"&gt;&lt;code&gt;actionStartActivity&lt;/code&gt;&lt;/a&gt;) or send a broadcast (i.e &lt;a href="https://developer.android.com/reference/kotlin/androidx/glance/appwidget/action/package-summary#actionsendbroadcast" rel="noopener noreferrer"&gt;&lt;code&gt;actionSendBroadcast&lt;/code&gt;&lt;/a&gt;) also.&lt;/p&gt;




&lt;h2&gt;
  
  
  Different size
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://developer.android.com/reference/kotlin/androidx/glance/appwidget/SizeMode.Single" rel="noopener noreferrer"&gt;&lt;strong&gt;SizeMode.Single&lt;/strong&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;When the widget size mode is &lt;code&gt;SizeMode.Single&lt;/code&gt; then the &lt;code&gt;GlanceAppWidget&lt;/code&gt; provides a single UI. The width and height of the app widget will be the minimum width and height given in &lt;a href="https://developer.android.com/reference/android/appwidget/AppWidgetProviderInfo" rel="noopener noreferrer"&gt;app widget info&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F89gx801lhmqva14x2y9z.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F89gx801lhmqva14x2y9z.gif" alt="Single size mode widget demo" width="800" height="640"&gt;&lt;/a&gt;&lt;em&gt;Single size mode widget demo&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://developer.android.com/reference/kotlin/androidx/glance/appwidget/SizeMode.Exact" rel="noopener noreferrer"&gt;&lt;strong&gt;SizeMode.Exact&lt;/strong&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;When the widget size mode is &lt;code&gt;SizeMode.Exact&lt;/code&gt; then the GlanceAppWidget provides a UI for each size the App Widget can be. (i.e any one of the possible size from supported grid. &lt;a href="https://developer.android.com/develop/ui/views/appwidgets/layouts#anatomy_determining_size" rel="noopener noreferrer"&gt;click here for more detail&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3wpvd3q0htd21mc68uum.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3wpvd3q0htd21mc68uum.gif" alt="Exact size mode widget demo" width="901" height="721"&gt;&lt;/a&gt;&lt;em&gt;Exact size mode widget demo&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://developer.android.com/reference/kotlin/androidx/glance/appwidget/SizeMode.Responsive" rel="noopener noreferrer"&gt;&lt;strong&gt;SizeMode.Responsive&lt;/strong&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;When the widget size mode is SizeMode.Responsive then the GlanceAppWidget provides a UI for a fixed set of sizes.&lt;br&gt;
SizeMode.Responsive takes a set of fixed sizes from it's constructor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftd8ofzyrwjlglj1zwkfm.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftd8ofzyrwjlglj1zwkfm.gif" alt="Responsive size mode widget demo" width="901" height="721"&gt;&lt;/a&gt;&lt;em&gt;Responsive size mode widget demo&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Other updates on the widget framework&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Apart from the Glance framework the existing widget framework also got some really good updates. lets see some of those.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Scalable widget preview&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;In Android 11 or below &lt;code&gt;android:previewImage&lt;/code&gt; is used to show how the widget would look like&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Since it’s an image, every time we update the widget design we have to change the image as well which would require some design effort.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Starting from Android 12 the widget preview can be derived from an XML layout, which results in better accuracy in reflecting how the actual widget will look like&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;appwidget-provider
    ...
    android:description="@string/app_widget_description"
    android:targetCellWidth="3"
    android:targetCellHeight="3"
    android:previewLayout="@layout/my_widget_preview"&amp;gt;
&amp;lt;/appwidget-provider&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F0%2AHiJDDi_CLgnUcvik" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F0%2AHiJDDi_CLgnUcvik" alt="**Ref** [https://developer.android.com/develop/ui/views/appwidgets/enhance](https://developer.android.com/develop/ui/views/appwidgets/enhance)" width="545" height="457"&gt;&lt;/a&gt;Ref::&lt;a href="https://developer.android.com/develop/ui/views/appwidgets/enhance" rel="noopener noreferrer"&gt;https://developer.android.com/develop/ui/views/appwidgets/enhance&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Other updates to widget framework&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Take a look &lt;a href="https://developer.android.com/develop/ui/views/appwidgets/enhance" rel="noopener noreferrer"&gt;here&lt;/a&gt; to enhance your app-widgets and explore more updates on the widget framework starting from Android 12.&lt;/p&gt;




&lt;p&gt;Repo with all the code samples is available here:&lt;br&gt;
&lt;a href="https://github.com/sridhar-sp/android-playground/tree/main/" rel="noopener noreferrer"&gt;https://github.com/sridhar-sp/android-playground/tree/main/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An important thing to notice here is that even though this change seems like. a breaking change none of the existing android widget frameworks was modified this entire Glance framework works on top of the existing android widget framework.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://android-developers.googleblog.com/2021/12/announcing-jetpack-glance-alpha-for-app.html" rel="noopener noreferrer"&gt;https://android-developers.googleblog.com/2021/12/announcing-jetpack-glance-alpha-for-app.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>jetpackcompose</category>
      <category>glance</category>
      <category>appwidgets</category>
    </item>
    <item>
      <title>Build a Modern Android Splash Screen</title>
      <dc:creator>Sridhar Subramani</dc:creator>
      <pubDate>Sun, 29 Dec 2024 08:54:47 +0000</pubDate>
      <link>https://dev.to/sridharsubramani/build-a-modern-android-splash-screen-9mf</link>
      <guid>https://dev.to/sridharsubramani/build-a-modern-android-splash-screen-9mf</guid>
      <description>&lt;p&gt;Android released official support to show the splash screen.&lt;/p&gt;

&lt;p&gt;This new splash screen support is added in Android 12, but the same can be used in earlier versions of Android using the &lt;a href="https://developer.android.com/jetpack/androidx/releases/core#core_splashscreen_version_10_2" rel="noopener noreferrer"&gt;splash screen support library&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To demo various aspects of the splash screen API, I’ve created a &lt;a href="https://github.com/sridhar-sp/android-splash-screen-demo" rel="noopener noreferrer"&gt;sample app&lt;/a&gt; with Jetpack Compose (&lt;a href="https://raw.githubusercontent.com/sridhar-sp/android-splash-screen-demo/main/docs/images/splash_preview.png" rel="noopener noreferrer"&gt;final splash screen preview&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Let’s begin!&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1
&lt;/h2&gt;

&lt;p&gt;Let us start by adding the splash screen &lt;a href="https://developer.android.com/jetpack/androidx/releases/core#core_splashscreen_version_10_2" rel="noopener noreferrer"&gt;dependency&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;implementation 'androidx.core:core-splashscreen:1.0.1'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2
&lt;/h2&gt;

&lt;p&gt;The next steps would be to create a splash screen icon and an optional brand image.&lt;/p&gt;

&lt;p&gt;We will discuss later on how to create the splash screen icon and make it appear center on the screen—in the &lt;code&gt;Extras&lt;/code&gt; section at the bottom of the screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3
&lt;/h2&gt;

&lt;p&gt;Once we have the image asset ready, we can apply the same using the splash screen theme.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    &amp;lt;style name="Theme.Splash.Starting" parent="Theme.SplashScreen"&amp;gt;
        &amp;lt;item name="windowSplashScreenBackground"&amp;gt;
            @color/splash_screen_background
        &amp;lt;/item&amp;gt;
        &amp;lt;item name="windowSplashScreenAnimatedIcon"&amp;gt;
            @drawable/ic_splash_icon
        &amp;lt;/item&amp;gt;
        &amp;lt;item name="windowSplashScreenAnimationDuration"&amp;gt;
            @integer/splash_screen_animation_duration
        &amp;lt;/item&amp;gt;
        &amp;lt;item name="postSplashScreenTheme"&amp;gt;
            @style/Theme.App.NoActionBar
        &amp;lt;/item&amp;gt;
        &amp;lt;!--Android 12 specific styles (put this in values-v31) --&amp;gt;
        &amp;lt;item name="android:windowSplashScreenBrandingImage"&amp;gt;
            @drawable/ic_brand_icon
        &amp;lt;/item&amp;gt;
        &amp;lt;!--Android 12 specific styles (put this in values-v31) --&amp;gt;
    &amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Anatomy of splash screen&lt;/code&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcuwxfx22qua6idfc3hzo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcuwxfx22qua6idfc3hzo.png" alt="Anatomy of splash screen" width="800" height="810"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s take a look at the style attributes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;windowSplashScreenBackground&lt;/code&gt; — As you have probably guessed, it’s the background colour that will occupy the whole screen.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;windowSplashScreenAnimatedIcon&lt;/code&gt; — This can either be a normal vector icon or an animated vector icon. Note: An animated vector icon will only show animation on Android 12+ devices, older API devices will simply show the static icon without any animation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;windowSplashScreenAnimationDuration&lt;/code&gt; — The duration of the animated icon. The recommended duration is 1,000 milliseconds.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;postSplashScreenTheme&lt;/code&gt; — This theme is used once the app is loaded. It’s required because we will set the launcher activity theme as Theme.SplashScreen in the app manifest — which works great for showing a splash screen, an animated logo, and a brand icon. However, once the app is loaded, we would get an exception if we didn’t use Theme.AppCompat or Theme.MaterialComponents . Hence, this attribute comes to the rescue.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;windowSplashScreenBrandingImage&lt;/code&gt; — Brand image to show bottom of the screen (only available from Android 12).&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4
&lt;/h2&gt;

&lt;p&gt;Now set the launcher activity theme as &lt;strong&gt;Theme.Splash.Starting&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    &amp;lt;activity
        android:name=".MainActivity"
        android:exported="true"
        android:label="@string/app_name"
        android:theme="@style/Theme.Splash.Starting"&amp;gt;
        &amp;lt;intent-filter&amp;gt;
            &amp;lt;action android:name="android.intent.action.MAIN" /&amp;gt;

            &amp;lt;category android:name="android.intent.category.LAUNCHER" /&amp;gt;
        &amp;lt;/intent-filter&amp;gt;
    &amp;lt;/activity&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5
&lt;/h2&gt;

&lt;p&gt;And finally, inside MainActivity &lt;code&gt;onCreate&lt;/code&gt; method add &lt;strong&gt;&lt;em&gt;installSplashScreen()&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        installSplashScreen()

        setContent {
            AndroidSplashScreenTheme {
                 Greeting("Android splash screen demo")
            }
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Other features
&lt;/h2&gt;

&lt;p&gt;Now that we have completed setting up our new Android splash screen, we can take a look at the other features that come with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Control splash screen visibility.
&lt;/h3&gt;

&lt;p&gt;The splash screen API provides support to keep the splash screen visible as long as we want it.&lt;/p&gt;

&lt;p&gt;Let’s say we need to fetch some resources that are crucial to rendering our screen, and without those resources, there is no use showing the UI. In this case, we can defer showing our application’s UI until our resources are fetched and ready.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    installSplashScreen().setKeepVisibleCondition {
        mainViewModel.isScreenLoading.value
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  setKeepVisibleCondition
&lt;/h3&gt;

&lt;p&gt;This method takes a callback which returns a boolean to tell whether to keep the screen on or not. True value indicates the splash screen should remain as is, and False would indicate dismissal of the splash screen.&lt;/p&gt;

&lt;p&gt;SplashScreen API will regularly call this callback to check whether to keep the splash screen or dismiss it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Animate screen content when the splash screen is dismissing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    installSplashScreen()
    .setOnExitAnimationListener { splashScreenViewProvider -&amp;gt;
        // Animation code
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Extras&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Let us discuss how to create the splash screen image.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcuwxfx22qua6idfc3hzo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcuwxfx22qua6idfc3hzo.png" alt="Anatomy of splash screen" width="800" height="810"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As we can see in the splash screen icon, section 1 is masked by section 3&lt;/p&gt;

&lt;p&gt;If we apply our icon as is, it will zoom out and stretch out.&lt;/p&gt;

&lt;p&gt;So how do we fix this? We need to create an icon that caters to this masked padding.&lt;/p&gt;

&lt;p&gt;Let’s say we have a splash screen icon of 54x54, then we would be creating a wrapper layer with a size of 108x108 (double the actual icon) and placing our icon content in the centre of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    &amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;
    &amp;lt;vector
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:name="vector"
        android:width="108dp"
        android:height="108dp"
        android:viewportWidth="108"
        android:viewportHeight="108"&amp;gt;
        &amp;lt;!--Optional background spanning full 108x108 dp--&amp;gt;
        &amp;lt;path
            android:name="background"
            android:pathData="M 0 0 L 108 0 L 108 108 L 0 108 Z"
            android:fillColor="@color/splash_icon_background"
            android:strokeWidth="1"/&amp;gt;
       &amp;lt;!--Splash screen icon placed in the centre of the screen--&amp;gt;
        &amp;lt;group
            android:name="group_icon"
            android:pivotX="54"
            android:pivotY="54"&amp;gt;
            &amp;lt;path
                android:name="path_pi"
                android:pathData="M 30 44.591 L......."
                android:fillColor="@color/splash_icon_color"
                android:strokeWidth="1"/&amp;gt;
        &amp;lt;/group&amp;gt;
    &amp;lt;/vector&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it, folks. Thanks for reading.&lt;/p&gt;




&lt;h2&gt;
  
  
  Source Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/sridhar-sp/android-splash-screen-demo" rel="noopener noreferrer"&gt;https://github.com/sridhar-sp/android-splash-screen-demo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>androiddev</category>
      <category>kotlin</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
