<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ashwini Kumar's Tech Blog 📝]]></title><description><![CDATA[🎯 Engineering towards excellence every single day]]></description><link>https://blogs.reactivedroid.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1700460633493/G0yG0TDMO.png</url><title>Ashwini Kumar&apos;s Tech Blog 📝</title><link>https://blogs.reactivedroid.com</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 15:24:52 GMT</lastBuildDate><atom:link href="https://blogs.reactivedroid.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Hard-earned Learnings From Navigating Google Play App Rejections]]></title><description><![CDATA[Introduction
Whether you’re an Android developer or a release manager, encountering app rejection is a familiar challenge. You may have found yourself pondering the intricacies of Google Play’s review process: How exactly are apps evaluated? Is it a ...]]></description><link>https://blogs.reactivedroid.com/hard-earned-learnings-from-navigating-google-play-app-rejections</link><guid isPermaLink="true">https://blogs.reactivedroid.com/hard-earned-learnings-from-navigating-google-play-app-rejections</guid><category><![CDATA[app rejection]]></category><category><![CDATA[Android]]></category><category><![CDATA[publish app]]></category><category><![CDATA[Google Play Console]]></category><category><![CDATA[compliance ]]></category><category><![CDATA[Policy]]></category><category><![CDATA[fintech]]></category><dc:creator><![CDATA[Ashwini Kumar]]></dc:creator><pubDate>Tue, 30 Jul 2024 08:36:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Ux-k3_A1YMw/upload/426ae00ec4131e3bf27f2cd272d08e5d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Whether you’re an Android developer or a release manager, encountering app rejection is a familiar challenge. You may have found yourself pondering the intricacies of Google Play’s review process: How exactly are apps evaluated? Is it a manual review, an automated system, or a combination of both?</p>
<p>With over a decade of experience in the mobile engineering industry, I’ve had the opportunity to work across various domains such as eCommerce, OTT, Gaming, and FinTech. In this article, I’ll share the valuable lessons I’ve learned from numerous app submissions to Google Play, spanning thousands of releases and multiple conversations with the Google Play Experience team throughout my career. These insights are driven by the passion and dedication of the Google Play Policy Experience team, who work tirelessly to understand developers and collaboratively resolve issues to expedite app approvals and support business operations. Additionally, I’ll provide key insights to help you streamline your review cycle and avoid future rejections. Join me on this exciting journey! ☕️🍿</p>
<blockquote>
<p>Failure is instructive. The person who really thinks learns quite as much from his failures as from his successes. – John Dewey</p>
</blockquote>
<h2 id="heading-understand-and-follow-googles-policies">Understand and Follow Google’s Policies</h2>
<h3 id="heading-app-access">App Access</h3>
<p>To effectively review your app, Google Play must have unrestricted access to all its components. If any part of your app is restricted, such as sections that require login credentials, you must provide clear instructions on how access can be granted. Historically, because the Play Review team operates globally and can review your app from any country, developers often had to provide exclusive login credentials and expose their infrastructure globally to avoid app rejections. This cumbersome process is no longer necessary.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Gotcha #1. Region-centric review:</strong> When submitting your app to the Play Console, it’s crucial to specify if access is restricted to certain countries. For example, if your app is designed exclusively for the Indian market, make sure to include instructions that specify app access should be reviewed within the IN region in the <code>Instruction Name</code> field. This ensures that the Play reviewer will assess your app in the designated region only. Please note, that this approach is effective only if your app supports VPN connections, enabling the reviewer to simulate access from the specified region.</div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721565134807/cf49fd38-c4ff-4c11-8e33-3c76f9c0697e.png" alt="Play Console -&gt; App Access" class="image--center mx-auto" /></p>
<h3 id="heading-sensitive-permissions"><strong>Sensitive Permissions</strong></h3>
<p>For the past five years, I have been immersed in the FinTech industry, navigating the distinctive challenges of developing apps in this domain. Ensuring seamless business operations requires strict adherence to numerous policies and internal audits, encompassing guidelines from the <a target="_blank" href="https://www.rbi.org.in/">RBI (India’s Apex Bank)</a>, <a target="_blank" href="https://support.google.com/googleplay/android-developer/answer/9888170?sjid=16252363608837214208-AP">Google Play Personal Loan Policies</a>, and others.</p>
<p>If your app requires sensitive permissions like reading SMS, accessing contacts, storage, or location, you might encounter occasional hurdles in the approval process. During these times, the focus shifts to swiftly addressing the feedback and ensuring your app update goes live promptly. You’ll engage in a thorough review of Google’s guidelines, interpret their feedback, and make the necessary adjustments, working diligently to meet their standards and gain approval for your app update.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721632673906/f4f864a4-c41c-4120-bc13-70fca6f8e647.png" alt="App Rejection due to invalid or inaccurate permissions" class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Gotcha #2. Restricted permissions removal:</strong> If your app’s primary functionality involves disbursing personal loans to customers, it is crucial to stay updated with Google’s exclusive <a target="_blank" href="https://support.google.com/googleplay/android-developer/answer/9876821?hl=en">Personal Loan Policy</a>. This policy imposes strict restrictions on accessing sensitive permissions such as <code>READ_CONTACTS</code>, <code>READ_PHONE_NUMBERS</code>, and <code>ACCESS_FINE_LOCATION</code> et al. Failure to comply with these guidelines will result in the rejection of your app. Therefore, ensuring adherence to these policies is essential to avoid disruptions and maintain your app's availability on Google Play.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Gotcha #3. Be clear with your sensitive permission declaration: </strong>If your app uses sensitive permissions such as SMS or Call Log, you must complete the <a target="_blank" href="https://support.google.com/googleplay/android-developer/answer/9214102">permissions declaration form</a> on the Play Console under the SMS and Call Log permissions section. In this form, you need to provide clear and transparent explanations for why these permissions are necessary. Additionally, support your request with a video demonstration to illustrate the legitimate use of these permissions.</div>
</div>

<h4 id="heading-curious-case-of-sms-permissions-group">Curious case of SMS Permissions Group</h4>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721652342121/6d80b262-230b-428c-b4b4-065054285556.png" alt="App Rejection when the it's core functionality could not be verified correctly." class="image--center mx-auto" /></p>
<p>If your app uses any SMS-related sensitive permissions such as <code>READ_SMS</code>, <code>RECEIVE_SMS</code>, or <code>SEND_SMS</code>, it is crucial to be meticulous in how you convey this information in your permissions declaration form. Clearly articulate the necessity of these permissions and ensure your explanation aligns with Google Play's guidelines. Additionally, cross-verify your use case against the exception use cases provided by Google Play on their <a target="_blank" href="https://support.google.com/googleplay/android-developer/answer/10208820?hl=en&amp;sjid=12615347681491252573-AP#zippy=%2Cexceptions">policy page</a> to ensure compliance.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Gotcha #4. Be transparent with your rationale:</strong> If your app requests the READ_SMS permission and your use case revolves around analyzing transactional SMSs only (non-personal) for purposes such as creating a credit score using ML models, it is imperative to explicitly declare this in your rationale. Despite having app logic that filters out personal messages, Google Play cannot verify the accuracy of your claims. To avoid future rejections, you should clearly state in your rationale that your app reads all SMS messages. This transparency will help ensure your app remains compliant and reduces the risk of rejection.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Gotcha #5. Provide a declaration that suits your use-case:</strong> For the above use case, you must declare SMS-based money management as the core functionality in the permissions declaration form. This ensures that Google Play understands the primary purpose of accessing SMS data and helps align your app with its policy requirements.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Gotcha #6. Update declaration if your use-case changes:</strong> If your app requires the SEND_SMS permission in addition to the money management use case, you must update your declaration to specify SMS-based financial transactions on the permissions declaration form. The SEND_SMS permission is typically used by apps for transaction verification via Device/SIM Binding and for providing UPI functionalities. If your app does not include these features, you should utilize the <a target="_blank" href="https://developer.android.com/guide/components/intents-common#SendMessage">SMS Intent</a> for sending SMS or the <a target="_blank" href="https://developers.google.com/identity/sms-retriever/overview">SMS Retriever API</a> for reading SMS to avoid app rejections.</div>
</div>

<h3 id="heading-data-safety-declaration">Data Safety Declaration</h3>
<p>To ensure greater transparency for your app’s users, Google Play requires you to clearly declare what user data your app collects or shares and to highlight your app’s key privacy and security practices. The information you provide in your Data Safety declaration is thoroughly reviewed by Google Play and will be prominently displayed in the Data Safety section of your app’s listing on the Play Store. This helps users make informed decisions about their data privacy and security when using your app.</p>
 <div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"> <strong>Gotcha #7. Ensure clear &amp; compliant account deletion process:</strong> In this section, if your app requires users to create an account, you must also provide clear steps for data and account deletion. The account deletion URL must link to an exclusive web page, not a deep link within your app’s Need Help section. This ensures a straightforward and transparent process for users wishing to delete their accounts and data, aligning with Google Play’s data privacy requirements.</div>
</div>

<p><a target="_blank" href="https://play.google.com/store/apps/datasafety?id=com.citrus.citruspay"><img src="https://cdn-images-1.medium.com/max/1600/1*irkL9KHEhr7GiHYcDLWzaQ.png" alt="LazyPay's Data Safety Declaration as visible on Play Store" /></a></p>
<h3 id="heading-financial-services-declaration">Financial Services Declaration</h3>
<p>Suppose your app’s core functionality involves disbursing personal loans in India. In that case, you must comply with specific requirements and provide supplementary documentation as part of the Financial Features declaration within the Play Console. Upon Google’s request, you must furnish additional information or documents demonstrating your compliance with relevant regulatory and licensing requirements. For more details, please refer to the financial services declaration <a target="_blank" href="https://support.google.com/googleplay/android-developer/answer/9876821?hl=en">he</a><a target="_blank" href="https://support.google.com/googleplay/android-developer/answer/9876821?hl=en">re</a>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Gotcha #8. Compliance Requirements for RBI-Licensed Personal Loan Apps:</strong> If you are licensed by the <a target="_blank" href="https://www.rbi.org.in/">Reserve Bank of India (RBI)</a> to disburse personal loans, you must submit a copy of your license for our review. If your app functions solely as a platform facilitating money lending by registered<a target="_blank" href="https://www.rbi.org.in/commonperson/English/Scripts/FAQs.aspx?Id=1167">Non-Banking Financial Companies (NBFCs)</a> or banks, you must clearly indicate this in your declaration. Additionally, you must prominently disclose the names of all associated NBFCs and banks in your app’s store description, ensuring this information is also visible on the respective NBFC’s or bank’s official websites.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Gotcha #9. Developer Account:</strong> Additionally, you must ensure that the <a target="_blank" href="https://support.google.com/googleplay/android-developer/answer/13634081?hl=en">developer account name</a> on Play Console matches the registered business name provided in your Financial Services declaration and license. This alignment is crucial to prevent potential rejections in the future.</div>
</div>

<p><img src="https://cdn-images-1.medium.com/max/1600/1*Zq_eYhy-R_YVvTvT8VYHpQ.png" alt="Play Console -&gt; App Content -&gt; Financial Features" /></p>
<h2 id="heading-useful-tips-and-tricks">Useful Tips and Tricks</h2>
<p>In addition to the points mentioned above, there are several other key considerations that can help you navigate the challenges of app rejections more effectively. No one wants to see their hard work during development rejected due to policy or compliance issues. However, if it does happen, you can follow the tips below to expedite the approval process and avoid subsequent rejections:</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Gotcha #10. Update all tracks: </strong>If your app is rejected due to Play Policy violations, particularly for the usage of sensitive permissions, it is important to review the affected versions on the Play Console. You must submit a new version of the app and upload it across all tracks (Open, Closed, Alpha, Internal, Production) to ensure compliance. Even if a track is paused or inactive, it must be updated with the new app version. Currently, Google Play does not offer the option to delete unnecessary tracks for which a feature request has been raised.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Gotcha #11. Comply with re-submissions:</strong> Furthermore, for any app rejection due to policy violations, you must submit your updated app version to 100% for review in one go. Failing to do so will leave the affected version accessible to users, leading to additional rejections. You can cross-verify the affected versions of your app by navigating to <code>App Content -&gt; SMS and Call Log Permissions -&gt; View app bundles and APKs</code></div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721653673077/180e20b1-a8f3-4588-b321-8c8be9534b5f.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Gotcha #12. Avoid updates when the app is in review:</strong> To avoid further rejections, it is essential to submit the updated app across all channels from the outset. If you attempt to make updates while the app is already under review, the initial submission will still be considered, as Play Console does not incorporate updates made during the review process (a feature request for this has been raised). Therefore, if your initial submission does not cover all channels or is not sent out to 100% for review, the app version will likely be rejected.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Gotcha #13. Prefer Managed Publishing: </strong>Always opt for<strong> </strong><a target="_blank" href="https://support.google.com/googleplay/android-developer/answer/9859654?hl=en">Managed Publishing</a> on the Play Console. This feature enables you to better manage your backend and app deployments, addressing all the issues mentioned above more effectively. Additionally, it provides clear visibility into what is being submitted for review, whether it’s the app itself, store information, or app content.</div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721654214684/d9d3a2b5-4148-4eca-8c7e-09db9df369c7.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Gotcha #14. Update the version code:</strong> If your app is rejected, it is currently mandatory to upload a new version with an <em>updated version code</em>. This means the entire release process must be followed: the build pipeline must run, and the updated version must be uploaded across all tracks. Even if your app’s content is driven by the backend (<a target="_blank" href="https://aws.amazon.com/blogs/mobile/backends-for-frontends-pattern/">Backend-For-Frontend</a>) and there has been no code modification, a new version with a different version code is still required. A feature request has been raised with the Play Experience team to address this.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Gotcha #15. Avoid automation failures:</strong> If you have automated your build process to upload <code>.aab</code> files to the Play Console and encounter an existing app rejection, your CI/CD pipeline for uploading the next updated version will fail. In such cases, you will need to manually upload your <code>.aab</code> file.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Gotcha #16. Do not shy in taking help: </strong>Policies continually evolve as regulations change. This means that app approvals previously secured may not be valid for future updates, leading to unexpected rejections. When this happens and you are uncertain about the details in the rejection email from Play, your first course of action should be to raise a support ticket or contact support via phone.</div>
</div>

<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXedc7AasbMux595nbK2cRQnmEnqynRK8hgCyjNSn_hNRRxAJBpVokZpB50H5PBq7s3-oXoSs2tiUtBimiSuam170duNevjpFEwjsE35fJOqE6EUsXe3NbYE04igA2_-f4DlXvXn0JAN1i-sfdXMEooqwvo4?key=bF9BioG08pv3iYTaYjS5cA" alt="Play Console -&gt; Top Right Need help (? Icon)" class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Dealing with app rejections can be challenging, as it directly impacts your business. You want your app up and running as soon as possible, and I hope the insights provided in this article will help you resolve issues more quickly. Google’s commitment to enhancing user privacy and security ensures a <a target="_blank" href="https://play.google/developer-content-policy/">safe and trusted experience</a> for everyone. Therefore, it is essential to stay updated with the <a target="_blank" href="https://support.google.com/googleplay/android-developer/announcements/13412212?sjid=16252363608837214208-AP">latest Play Policy Updates</a>, which are available in the <a target="_blank" href="https://support.google.com/googleplay/android-developer/announcements/13412212?sjid=16252363608837214208-AP">Policy Center</a>. I will continue to update this article with new insights and tips as they arise. Until then, keep building 🚀</p>
<blockquote>
<p><em>Do you have additional tips and tricks up your sleeve? Please comment and share your ideas so the entire developer community can benefit and expedite the app approval process. 🤝</em></p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[From Experience to Infrastructure Engineering: A Tale of Kubernetes Certifications]]></title><description><![CDATA[Joining PayU Credit has been an incredible journey, where I have the honour of leading a team of exceptional engineers aligned with our business vision to create cutting-edge applications. As the Director of Engineering overseeing customer-facing web...]]></description><link>https://blogs.reactivedroid.com/from-experience-to-infrastructure-engineering-a-tale-of-kubernetes-certifications</link><guid isPermaLink="true">https://blogs.reactivedroid.com/from-experience-to-infrastructure-engineering-a-tale-of-kubernetes-certifications</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[Infrastructure management]]></category><category><![CDATA[Docker]]></category><category><![CDATA[scalability]]></category><category><![CDATA[System administration]]></category><category><![CDATA[Site Reliability Engineering]]></category><dc:creator><![CDATA[Ashwini Kumar]]></dc:creator><pubDate>Mon, 15 Apr 2024 12:45:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/jOqJbvo1P9g/upload/a4e354fd07065e3bb3c8ba020260e21a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Joining PayU Credit has been an incredible journey, where I have the honour of leading a team of exceptional engineers aligned with our business vision to create cutting-edge applications. As the Director of Engineering overseeing customer-facing web and mobile apps, my role extends beyond crafting seamless user experiences to ensuring our team’s continuous growth and productivity across various technological fronts.</p>
<p>For instance, our approach to app development is characterized by innovation and efficiency. Take our flagship <a target="_blank" href="https://play.google.com/store/apps/details?id=com.citrus.citruspay">LazyPay</a> and <a target="_blank" href="https://play.google.com/store/apps/details?id=com.gopaysense.android.boost">Paysense</a> Android apps, for example, both meticulously crafted using Jetpack Compose with Kotlin Coroutines at their core. Similarly, our <a target="_blank" href="https://apps.apple.com/in/app/lazypay-buy-now-pay-later/id1419990272">LazyPay iOS app</a> leverages SwiftUI and Combine to deliver highly responsive user interactions.</p>
<p>But our commitment to excellence doesn’t stop at the front-end. Across both mobile and web applications, we’ve prioritized modularization, laying the groundwork for a scalable architecture that fosters rapid development. By embracing modular frameworks, libraries, micro-apps, and micro-frontends, our team has not only streamlined development processes but also significantly reduced time-to-market for new features and enhancements.</p>
<p>Behind the scenes, our Mobile Backend Service, aptly named Sauron, forms the backbone of our app ecosystem. With a focus on performance, scalability, and availability, the team ensures that Sauron operates at peak efficiency to support our applications’ demands. My interest in Kubernetes(K8s) stemmed from a direct involvement in optimizing Sauron’s infrastructure costs, a journey that has further deepened my appreciation for scalable and cost-effective solutions.</p>
<p>As I delve deeper into the actual problem and its solution, I will also go through the journey of why and how I earned the CKA and CKAD K8s certifications in detail. This journey reflects my ongoing commitment to mastering new technologies and driving continuous improvement. It’s a testament to the belief that growth lies beyond our comfort zones, and I strive to inspire others to embrace this philosophy fearlessly.</p>
<blockquote>
<p>The more that you read, the more things you will know. The more that you learn, the more places you will go. — Dr Seuss</p>
</blockquote>
<h2 id="heading-the-trigger-sauron-excessive-logging-issue">The Trigger: Sauron Excessive Logging Issue 📈</h2>
<p>Our Mobile Backend Service, Sauron, functions as a Backend-for-Frontend (BFF) layer, orchestrating interactions with our internal microservices. While devoid of any business logic, Sauron optimizes caching and transforms service responses to align with client requirements. Despite its pivotal role, Sauron was burdened with a substantial log ingestion volume, consuming approximately 600GB of logs daily, which surged to a staggering 1.1TB during peak repayment cycles.</p>
<p>To manage this influx, we had initially over-provisioned infrastructure resources, including production clusters and <a target="_blank" href="https://www.elastic.co/elasticsearch">Elasticsearch</a>, to accommodate the heightened demand. However, the escalating costs prompted us to reassess our approach. Recognizing the need to curb infrastructure expenses, our team embarked on a mission to reduce log ingestion.</p>
<h3 id="heading-reducing-the-log-ingestion-by-90">Reducing the log ingestion by ~90% 🚀</h3>
<p>Our efforts to optimize log ingestion yielded substantial benefits, leading to significant cost savings through infrastructure scaling. We implemented several key initiatives to achieve these results:</p>
<ol>
<li><p>Eliminated aspect logging from API calls to reduce unnecessary verbosity.</p>
</li>
<li><p>Trimmed down cache hit-and-miss logs, focusing only on essential data.</p>
</li>
<li><p>Reduced excessive response logging to minimize log size.</p>
</li>
<li><p>Implemented smarter logging levels (Verbose, Debug, Warning, Error) to prioritize critical information.</p>
</li>
<li><p>Identified and removed redundant API calls and their corresponding logs.</p>
</li>
<li><p>Utilized local in-memory cache to reduce the need for multiple internal service calls.</p>
</li>
<li><p>Established a meticulous and automated code review process to identify and address logging issues proactively.</p>
</li>
<li><p>Tweaked the <a target="_blank" href="https://www.elastic.co/beats/filebeat">Filebeat</a> configuration using <a target="_blank" href="https://www.elastic.co/guide/en/beats/filebeat/current/drop-fields.html">drop fields.</a></p>
</li>
</ol>
<p>These initiatives were implemented gradually and methodically, ensuring minimal disruption to production environments and preserving our debugging capabilities. Over the past six months, our efforts have led to a remarkable reduction in log size, reaching 50GB and 100GB on regular and repayment cycle days, respectively. Moreover, we achieved a 60% reduction in infrastructure usage, resulting in substantial cost savings for the organization.</p>
<p><img src="https://cdn-images-1.medium.com/max/1600/1*c-T54STl8_DurdObSZDRmQ.png" alt="Infra and Cost Optimisation on Sauron(Mobile Backend Service)" /></p>
<h3 id="heading-unforeseen-downtime-navigating-challenges-in-optimization">Unforeseen Downtime: Navigating Challenges in Optimization ⚙️</h3>
<p>However, our journey wasn’t without its challenges. Despite our best efforts, one particular initiative led to an unexpected hiccup. Following the implementation of various optimizations, we conducted a scale-down activity to assess performance and scalability. Unfortunately, this decision resulted in an unforeseen production downtime for a couple of hours.</p>
<p>As we investigated the root cause, we discovered that our assumptions about the behaviour of the <a target="_blank" href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/">Horizontal Pod Autoscaler</a>(HPA) were flawed. The <a target="_blank" href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/">HPA</a> failed to trigger auto-scaling due to preset limits on the number of requests each POD could manage. Despite the performance metrics like CPU and memory remaining within acceptable thresholds, the POD couldn’t process the incoming requests as expected. This revelation was a pivotal ‘Aha!’ moment for me, prompting a deep dive into Kubernetes and a pursuit of relevant certifications.</p>
<p>We discovered that the <a target="_blank" href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/">HPA</a> wasn’t equipped to meet our specific needs and wasn’t suitable for our use cases. We needed triggers beyond standard performance metrics, such as the number of requests or queue lengths, to effectively auto-scale our deployments. To prevent similar incidents in the future, we implemented <a target="_blank" href="https://keda.sh/">Keda</a>, an event-driven auto scaler capable of dynamically adjusting scaling based on multiple factors and attributes.</p>
<h2 id="heading-why-kubernetes-matters-a-personal-journey">Why Kubernetes Matters: A Personal Journey 💡</h2>
<ul>
<li><p><strong>Cloud-native apps:</strong> As mobile apps increasingly rely on cloud-based services, understanding K8s can help me to design and deploy cloud-native applications that scale and perform well.</p>
</li>
<li><p><strong>Backend Service:</strong> Since we faced an issue during one of the scale-down activities of our Sauron service earlier, knowing Kubernetes from the ground zero would help me to communicate effectively <a target="_blank" href="https://keda.sh/">to</a> our backend team in optimising the services further.</p>
</li>
<li><p><strong>Containerization:</strong> K8s is built around containerisation(e.g. <a target="_blank" href="https://www.docker.com/">Docker</a>) and that’s why understanding it better can allow to package the app’s code &amp; dependencies into a single light-weight container, making it easier to manage and deploy.</p>
</li>
<li><p><strong>Micro-services architecture:</strong> K8s is well-suited for the microservices architecture which is increasingly popular in the front-facing apps. Knowing it better can help in designing and deploying the scalable distributed systems that consist of multiple services communicating with each other.</p>
</li>
<li><p><strong>DevOps collaboration:</strong> K8s is a key tool for DevOps teams. By learning it, I can now collaborate more effectively with my DevOps colleagues and improve the overall efficiency of our team.</p>
</li>
<li><p><strong>Career Growth:</strong> Knowing K8s can open up new learning and career opportunities in cloud computing, DevOps and Distributed systems. While I don’t aim to pursue my career in DevOps, having an advanced knowledge around the infra-structure and its internal workings can open doors to many untouched realms of engineering.</p>
</li>
<li><p><strong>Better communication with Stakeholders:</strong> With K8s knowledge, I can collaborate more effectively with the stakeholders, including product managers, customers and executives about the technical aspects of the backend service. I can now debate around the infra capabilities and its concepts in a more clear and concise manner.</p>
</li>
<li><p><strong>Improved debugging skills:</strong> Understanding K8s can help in debugging production issues much faster and more efficiently as I will have a deeper understanding of the underlying infrastructure and services. This can in-turn help in improving the overall quality and reliability of our mobile and web apps.</p>
</li>
<li><p><strong>Future-proofing:</strong> K8s is a rapidly evolving field, and learning it can help me future-proof my skills and stay ahead of the curve in the industry. Having expertise on the mobile engineering aided with K8s knowledge can help me in taking advantage of new technologies and trends as they emerge.</p>
</li>
</ul>
<h2 id="heading-my-certification-journey">My Certification Journey 🎓</h2>
<p>Learning Kubernetes isn’t just a journey — it’s an adventure filled with challenges, discoveries, and moments of triumph. As someone entrenched in the world of experience engineering, diving into Kubernetes meant immersing myself in every layer of its infrastructure. From the fundamental building blocks to the intricate inner workings, I embarked on a journey to unravel the mysteries of Kubernetes and unlock its full potential.</p>
<p>But learning Kubernetes wasn’t just about acquiring knowledge — it was about pushing boundaries and expanding horizons. As I delved deeper into the intricacies of container orchestration, I realized the immense value of formalizing my expertise through certification. It wasn’t just about validating my skills — it was about embracing a new chapter in my journey as a technologist, driven by a passion for continuous learning and growth.</p>
<h3 id="heading-road-to-ckad-certified-kubernetes-application-developerhttpswwwcredlycombadges58549c27-9e4d-49ba-8847-2667947d7df2">Road to <a target="_blank" href="https://www.credly.com/badges/58549c27-9e4d-49ba-8847-2667947d7df2">CKAD: Certified Kubernetes Application Developer</a></h3>
<ul>
<li><p>Completed <a target="_blank" href="https://www.udemy.com/course/learn-docker/">Docker for the Absolute Beginner course</a>, gaining foundational knowledge in containerization.</p>
</li>
<li><p>Completed <a target="_blank" href="https://www.udemy.com/course/learn-kubernetes/">Kubernetes for the Absolute Beginner course</a>, acquiring a comprehensive understanding of Kubernetes fundamentals.</p>
</li>
<li><p>Successfully completed the <a target="_blank" href="https://www.udemy.com/course/certified-kubernetes-application-developer/">Certified Kubernetes Application Developer (CKAD) course</a>, including extensive practice tests and Mock Exams to solidify skills.</p>
</li>
<li><p>Engaged in hands-on practice with <a target="_blank" href="https://github.com/vim/vim">VIM</a>, mastering essential shortcuts crucial for efficient coding and exam success.</p>
</li>
<li><p>Completed CKAD exercises by <a target="_blank" href="https://github.com/dgkanatsios/CKAD-exercises">Dimitris-Ilias Gkanatsios</a> and <a target="_blank" href="https://github.com/bbachi/CKAD-Practice-Questions">Bhargav Bachina</a>, reinforcing practical skills and exam readiness.</p>
</li>
<li><p>Practiced <a target="_blank" href="https://killercoda.com/killer-shell-ckad">Killercoda</a> and <a target="_blank" href="https://killer.sh/">KillerShell</a> scenarios for CKAD, simulating real-world exam conditions and honing problem-solving abilities.</p>
</li>
<li><p>Learned <a target="_blank" href="https://helm.sh/">HELM</a> to effectively deploy and manage Kubernetes applications, further expanding proficiency in container orchestration.</p>
</li>
</ul>
<p>I dedicated my off-hours and any available leisure time outside of my regular work schedule to diligent practice. This commitment bore fruit when I successfully passed the CKAD certification exam on my first attempt, achieving an impressive score of 88 out of 100. 🥳</p>
<p><a target="_blank" href="https://www.credly.com/badges/58549c27-9e4d-49ba-8847-2667947d7df2"><img src="https://cdn-images-1.medium.com/max/1600/1*vJh6aJiKApENHsPbKcPmnA.jpeg" alt="Earners of this designation demonstrated the skills, knowledge and competencies to perform the responsibilities of a Kubernetes Application Developer. Earners are able to define application resources and use core primitives to build, monitor, and troubleshoot scalable applications and tools in Kubernetes. The skills and knowledge demonstrated by earners include Core Concepts, Configuration, Multi-Container Pods, Observability, Pod Design, Services &amp; Networking, State Persistence." /></a></p>
<h3 id="heading-road-to-cka-certified-kubernetes-administratorhttpswwwcredlycombadges58549c27-9e4d-49ba-8847-2667947d7df2">Road to <a target="_blank" href="https://www.credly.com/badges/58549c27-9e4d-49ba-8847-2667947d7df2">CKA: Certified Kubernetes Administrator</a></h3>
<p>Achieving my CKAD certification was a moment of genuine satisfaction, considering I ventured into a domain completely unfamiliar to me. As I delved deeper into Kubernetes concepts, my interest in mastering the intricacies of Kubernetes intensified. This curiosity led me to set my sights on obtaining the <a target="_blank" href="https://training.linuxfoundation.org/certification/certified-kubernetes-administrator-cka/">CKA: Certified Kubernetes Administrator Certification</a>, a challenging yet rewarding pursuit.</p>
<ul>
<li><p>Successfully completed the <a target="_blank" href="https://www.udemy.com/course/certified-kubernetes-administrator-with-practice-tests/">Certified Kubernetes Administrator (CKA) course</a> and its associated lab with Mock Exams on KodeKloud.</p>
</li>
<li><p>Learnt <a target="_blank" href="https://github.com/kelseyhightower/kubernetes-the-hard-way">K8s the hard-way by Kelsey Hightower</a>.</p>
</li>
</ul>
<p>Transitioning from CKAD to CKA was a natural progression, considering my solid foundation in Kubernetes Application Development. The CKA exam, with its emphasis on both administrative and development aspects of Kubernetes, posed a new challenge. This certification requires a comprehensive understanding of security, roles, cluster debugging, high availability, networking, and more. After diligently preparing for several months following my CKAD certification, I undertook the CKA exam and was thrilled to achieve a score of 99 out of 100, exceeding even my own expectations. 🥳</p>
<p><a target="_blank" href="https://www.credly.com/badges/58549c27-9e4d-49ba-8847-2667947d7df2"><img src="https://cdn-images-1.medium.com/max/1600/1*GDMlUQyMv5EzcT1Wlg4s8Q.jpeg" alt="Earners of this designation demonstrated the skills, knowledge and competencies to perform the responsibilities of a Kubernetes Administrator. Earners demonstrated proficiency in Application Lifecycle Management, Installation, Configuration &amp; Validation, Core Concepts, Networking, Scheduling, Security, Cluster Maintenance, Logging / Monitoring, Storage, and Troubleshooting" /></a></p>
<h2 id="heading-unlocking-new-horizons-embracing-the-kubernetes-journey">Unlocking New Horizons: Embracing the Kubernetes Journey 🌱</h2>
<p>As I reflect on my journey from being a mobile engineer to earning CKA and CKAD certifications, I realize that Kubernetes has become an integral part of my skillset. The knowledge and experience I gained have not only helped me in my current role but have also opened up new opportunities for growth and collaboration. If you’re a developer irrespective of your domain expertise and looking to expand your horizons, I encourage you to embark on the Kubernetes journey. It may seem daunting at first, but with persistence and dedication, you’ll find that the benefits far outweigh the challenges. Here are some key takeaways from my experience:</p>
<ul>
<li><p>Kubernetes is not just for ops teams; it’s for anyone who wants to build scalable, reliable, and efficient systems.</p>
</li>
<li><p>Learning Kubernetes requires practice, patience, and persistence.</p>
</li>
<li><p>The Kubernetes community is vast and supportive; don’t be afraid to ask questions or seek help.</p>
</li>
<li><p>Kubernetes is a constantly evolving field; stay curious and keep learning.</p>
</li>
</ul>
<p>My journey with Kubernetes has been a transformative experience that has helped me grow both professionally and personally. I hope that my story will inspire you to take the leap and join the Kubernetes community. Embrace the journey, and get ready to unlock new possibilities in your career! 🚀</p>
<blockquote>
<p>During the recent LazyPay app overhaul, we strategically re-architected our API design, adopting a hybrid approach that seamlessly blends data and user interface elements. This shift enhances scalability and fosters extensibility, ensuring our app remains agile and adaptable to evolving user needs.</p>
<p>Our dedicated team meticulously crafted the <a target="_blank" href="https://play.google.com/store/apps/details?id=com.citrus.citruspay">LazyPay Android</a> and <a target="_blank" href="https://apps.apple.com/in/app/lazypay-buy-now-pay-later/id1419990272">iOS</a> apps, leveraging the latest technology stacks to deliver a seamless and immersive user experience. Check them out today on the respective app stores and share your valuable feedback! ❤️</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Faster media delivery to optimize app performance]]></title><description><![CDATA[As startups in India and across the world continue to build inspiring products to solve contrasting problems, the hunt to scale the apps for providing delightful user experiences also grows. They follow the inherent culture of moving fast and breakin...]]></description><link>https://blogs.reactivedroid.com/faster-media-delivery-to-optimize-app-performance-e09ce96c9757</link><guid isPermaLink="true">https://blogs.reactivedroid.com/faster-media-delivery-to-optimize-app-performance-e09ce96c9757</guid><category><![CDATA[Android]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[cloudinary]]></category><category><![CDATA[Performance Optimization]]></category><category><![CDATA[iOS]]></category><dc:creator><![CDATA[Ashwini Kumar]]></dc:creator><pubDate>Tue, 28 Mar 2023 08:10:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1697539401395/0243cb80-2354-4b41-ad25-be2e93a79973.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As startups in India and across the world continue to build inspiring products to solve contrasting problems, the hunt to scale the apps for providing delightful user experiences also grows. They follow the inherent culture of <em>moving fast and breaking things,</em> to build the MVP faster justifying the problem-solving and business strategies. <a target="_blank" href="https://effectiviology.com/premature-optimization/">Premature optimization</a> is consciously avoided to prove the mettle of the product being built. Once the idea is gracefully adopted, the journey of 1 to 100 begins. This article is a story of why and how the Experience Engineering Team migrated from <a target="_blank" href="https://aws.amazon.com/s3/">AWS S3</a> to <a target="_blank" href="https://cloudinary.com/">Cloudinary</a> to serve optimized media to our mobile and web apps.</p>
<h3 id="heading-challenges">Challenges</h3>
<p>When an idea is born, the app around it is built in the fastest and crudest way possible. Like other startups, LazyPay apps were developed in a similar manner. The media like images, videos, and animations(Lottie) being served on the apps were stored on AWS S3 which were consumed by apps directly through the S3 URLs. The brute-force approach obviously had problems at many levels.</p>
<ul>
<li><p><strong>Performance Bottlenecks:</strong> Although we backed our S3 buckets with <a target="_blank" href="https://aws.amazon.com/cloudfront/">AWS CloudFront</a> for utilizing caching and security gains, the apps were getting media of the same sizes that were stored by us leading to higher bandwidth consumption.</p>
</li>
<li><p><strong>Screen Size and Rendering Issues:</strong> To be highly responsive, the APIs should serve the response with millisecond latency. Unfortunately, this is not the case most of the time, especially over media. This becomes more painful when a user on a mobile device requests for an asset and an unwanted cache-miss leads to the slow screen rendering making the app unusable.</p>
</li>
<li><p><strong>Page Load Time:</strong> Since the media being served was not optimized, it directly affected our page load time leading to delayed interaction by the user with our product offerings. This became even worse when our users were connected to a 4G network and residing in any low connectivity zone or even roaming.</p>
</li>
<li><p><strong>Heavy Maintenance:</strong> To support multiple clients and optimize asset delivery, we were uploading <em>multiple variants</em> of the same image based on the platform(android/iOS/web) and device/browser density. For ex: If we had a banner for promoting <a target="_blank" href="https://www.lazypay.in/personal-loan">Xpress Loan</a> on the app, we would be uploading 7 variants of this image ranging from <code>mdpi</code> to <code>xxxhdpi</code> on Android and <code>1x</code> to <code>3x</code> on iOS respectively.</p>
</li>
<li><p><strong>Unstructured asset management:</strong> Since every client was handling the assets according to their own needs, there was no order to the files that were being uploaded, creating a mess of buckets and folders everywhere.</p>
</li>
</ul>
<h3 id="heading-cloudinaryhttpscloudinarycom-as-the-media-optimizer"><a target="_blank" href="https://cloudinary.com/">Cloudinary</a> as the Media Optimizer</h3>
<p><a target="_blank" href="https://aws.amazon.com/s3/">AWS S3</a> for cloud storage of assets is the right choice and we are sticking to it. However, we have laid out a consistent process around asset management and brought a true order to it. Also, to reduce the network footprint of our apps and enhance their performance, we have invested a lot in adopting Cloudinary across all of our clients.</p>
<blockquote>
<p><a target="_blank" href="https://cloudinary.com/">Cloudinary</a> is a powerful media hosting and optimization platform that delivers high-quality media without much maintenance effort.</p>
</blockquote>
<h4 id="heading-from-s3-via-cloudinary">From S3 via Cloudinary</h4>
<p>Connecting S3 to Cloudinary is pretty straightforward.</p>
<ol>
<li><p>Create<code>Media Source</code> on Cloudinary for the S3 bucket</p>
</li>
<li><p>Create an <code>Optimization Profile</code>on Cloudinary and attach the media source with it.</p>
</li>
<li><p>Provide the base transformation on the profile as one of the custom transformations.</p>
</li>
</ol>
<h4 id="heading-ease-of-maintenance">Ease of Maintenance</h4>
<p>The best part of using Cloudinary is its <strong><em>zero maintenance effort</em></strong>. Recall, when we said earlier that we were uploading 7 variants of an image to support multiple clients. Not anymore! With Cloudinary, we are now uploading only <em>one master image</em> which serves all our clients without compromising on the app performance.</p>
<p><a target="_blank" href="https://cloudinary.com/documentation/image_transformations"><strong>Cloudinary Transformations</strong></a></p>
<p>Cloudinary can serve different variants of the master image on the fly via <a target="_blank" href="https://cloudinary.com/documentation/image_transformations">transformations</a>. We have created <a target="_blank" href="https://cloudinary.com/documentation/named_transformations_tutorial">named transformations</a> for all of our clients to not only serve the optimized asset according to the device/browser but also shorten the image URLs. These transformations have helped significantly in lowering our maintenance efforts and increasing our productivity.</p>
<ul>
<li><p>On Android, we have 5 named transformations mapping with <code>mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi</code> created based on <a target="_blank" href="https://developer.android.com/reference/android/util/DisplayMetrics">display metrics</a>.</p>
</li>
<li><p>On iOS, we have 3 named transformations mapping with <code>1x, 2x and 3x</code> created based on the <a target="_blank" href="https://developer.apple.com/documentation/uikit/uiscreen/1617836-scale">scale factor</a>.</p>
</li>
<li><p>On the web, we have 3 named transformations mapping with <code>small, medium and large</code> created based on aspect ratio.</p>
</li>
</ul>
<blockquote>
<p><a target="_blank" href="https://play.google.com/store/apps/details?id=com.citrus.citruspay">LazyPay android app</a> requests an image on a medium display density device -&gt; Android app appends the transformation(<code>tx=t_droid_xhdpi</code>) dynamically on the requested Cloudinary image URL -&gt; Cloudinary checks for the transformed image in cache -&gt; If cache hits, serve the image, else transform the image from the master one on the fly, cache, and then serve.</p>
</blockquote>
<h4 id="heading-optimizations">Optimizations</h4>
<p>Cloudinary’s out-of-the-box <a target="_blank" href="https://cloudinary.com/documentation/image_optimization">image optimizations</a> helped in further reducing data consumption.</p>
<ul>
<li><p>An image can be served in any format <code>webp, avif</code> or <code>png</code> automatically by Cloudinary optimized based on clients (Android/iOS/Web).</p>
</li>
<li><p>Cloudinary’s <a target="_blank" href="https://cloudinary.com/documentation/advanced_url_delivery_options#multi_cdn_solutions">multi-CDN</a> helps in delivering the images faster and more reliably.</p>
</li>
<li><p>Bytes saved are bytes gained for processing other high-intensive APIs within the app. Using Cloudinary helped us to save <strong>70%</strong> of the bytes when compared to the originals. For eg: An image of 100 KB on S3 required only 30 KB to be served via Cloudinary.</p>
</li>
<li><p>Apart from the byte savings, we even monitored the performance of the assets being served through Cloudinary and compared them with AWS S3 through <a target="_blank" href="https://firebase.google.com/docs/perf-mon/">Firebase Performance Monitoring</a>. Monitoring insights suggested that image URLs served through Cloudinary were performing <strong>faster by 20%</strong> as compared to S3.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697539403207/13cbf743-b525-46e9-afa5-edae5c70e227.jpeg" alt /></p>
<h4 id="heading-go-to-market-through-controlled-feature-rollout">Go-to-market through controlled feature rollout 🚀</h4>
<p>Migration to Cloudinary was a big change for us and we wanted to tread very carefully before adopting it on all platforms. Launching in one shot might have given an experience where the user would not be seeing any asset on our app giving the worst user experience. Reasons could be many: Dirty Setup, Missing Media, Heavy Loads, etc. That’s why, we controlled the feature and rolled it out incrementally through <a target="_blank" href="https://firebase.google.com/docs/remote-config/">Firebase Remote Config</a>. Cloudinary’s error reporting facilitated the process by properly displaying the asset URLs giving HTTP 404s &amp; 400s. We kept on stabilizing the error count and increased the rollout only when the error graph had flattened out. While doing so, we also uncovered some new and age-old issues.</p>
<ul>
<li><p>HTTP 404 errors received on Cloudinary were for the assets that were never uploaded and missing on S3. This is a typical case of a hibernating bug finally waking up. We had to find those assets somehow, upload them on S3, and fix the errors.</p>
</li>
<li><p>Since we added a blanket transformation across all of our assets uploaded on S3, this impacted assets like <code>gif</code> or <code>pdf</code> giving HTTP 400 error due to increased MegaPixel count as we were asking for better quality gifs on high-density devices. <code>Error from Cloudinary: Maximum image size is 8 Megapixels. Requested 9.72 Megapixels</code><br />  We then added a filter to exclude the media types like <code>gif and pdf</code> from being transformed.</p>
</li>
</ul>
<h3 id="heading-wrap-up">Wrap up</h3>
<p>We are already using <a target="_blank" href="https://cloudinary.com/">Cloudinary</a> for serving assets on all of our mobile apps and slowly rolling it out to our web apps.</p>
<p>FinTech has many unique challenges and as we are progressing, we are sharpening our skills, re-architecting the apps, and solidifying our infrastructure to build for India scale. We are challenging the status quo at each moment all aimed at giving an awesome user experience. Stay tuned for the next one!</p>
]]></content:encoded></item><item><title><![CDATA[Dynamic Environment Switching on Android]]></title><description><![CDATA[How many times have you been asked to send a prod or staging pointed build to your quality engineering team?


How many times have you had to build and install another build to test a functionality pointing to different environments?

How many times ...]]></description><link>https://blogs.reactivedroid.com/dynamic-environment-switching-on-android-2048567e59c7</link><guid isPermaLink="true">https://blogs.reactivedroid.com/dynamic-environment-switching-on-android-2048567e59c7</guid><category><![CDATA[Android]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[android app development]]></category><dc:creator><![CDATA[Ashwini Kumar]]></dc:creator><pubDate>Wed, 12 Oct 2022 08:38:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/8OVDzMGB_kw/upload/725e41c1977e01b90a9ac1f11ffce559.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<ul>
<li>How many times have you been asked to send a prod or staging pointed build to your quality engineering team?</li>
</ul>
<ul>
<li><p>How many times have you had to build and install another build to test a functionality pointing to different environments?</p>
</li>
<li><p>How many times have your Backend engineers wanted to test the functionality during the development on their local setup?</p>
</li>
</ul>
<p>On numerous counts. Isn’t it? On every occasion, you had to start the tedious build process which will take its own sweet build time thus delaying the quality and testing turnaround time.</p>
<p>Not anymore! At <a target="_blank" href="https://play.google.com/store/apps/details?id=com.citrus.citruspay">LazyPay</a>, we developed an in-app flow to let anyone switch their environment without requiring new builds enhancing our developer and QA productivity. This article will help you understand the path we took and guide you accordingly in creating <em>One Build to Rule Them All</em> for your Android apps. 🍿 ☕️</p>
<h3 id="heading-conventional-process">Conventional Process ✍️</h3>
<p>In general, to support different environments, we will have a 1:1 mapping of <a target="_blank" href="https://developer.android.com/reference/tools/gradle-api/4.2/com/android/build/api/dsl/ProductFlavor"><code>productFlavor</code></a> the environment’s host URL. For the sake of consistency and to extract the best of Kotlin, we will be talking in terms of <a target="_blank" href="https://docs.gradle.org/current/userguide/kotlin_dsl.html">Gradle’s Kotlin DSL</a> for all build-related scripts. For eg. Say, our app should support two backend environments Production and Staging. This is what our <code>build.gradle.kts</code> looks like:</p>
<pre><code class="lang-kotlin">android {
  ...
  flavorDimensions.addAll(listOf(<span class="hljs-string">"env"</span>))
  productFlavors {
    create(<span class="hljs-string">"prod"</span>) {
        dimension = <span class="hljs-string">"env"</span>
        buildConfigField(<span class="hljs-string">"String"</span>, <span class="hljs-string">"HOST_URL"</span>, <span class="hljs-string">"https://prod.api.com"</span>)
        buildConfigField(<span class="hljs-string">"String"</span>, <span class="hljs-string">"SOME_KEY"</span>, <span class="hljs-string">"prod_key"</span>) 
   }
    create(<span class="hljs-string">"staging"</span>) {
        dimension = <span class="hljs-string">"env"</span>
        buildConfigField(<span class="hljs-string">"String"</span>, <span class="hljs-string">"HOST_URL"</span>, <span class="hljs-string">"https://staging.api.com"</span>)
        buildConfigField(<span class="hljs-string">"String"</span>, <span class="hljs-string">"SOME_KEY"</span>, <span class="hljs-string">"staging_key"</span>)     
    }
  }
  ...
}
</code></pre>
<p>When we create <code>prodDebug</code> and <code>stagingDebug</code> apps, each will be pointing to their respective environment's <code>HOST_URL</code>. To test a feature’s behaviour on multiple environments, we will build separate apps accordingly.</p>
<h3 id="heading-the-idea">The Idea💡</h3>
<p>On Android, when we insert any key-value pair under <code>buildConfigField</code> and compile the source, <code>BuildConfig.java</code> is auto-generated and this changes according to our <code>buildFlavor</code> defined values. For the example above, <code>BuildConfig.java</code> will look something like this for <code>stagingDebug</code>build variant:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BuildConfig</span> </span>{
<span class="hljs-keyword">public</span> static <span class="hljs-keyword">final</span> boolean DEBUG = <span class="hljs-built_in">Boolean</span>.parseBoolean(<span class="hljs-string">"true"</span>);
<span class="hljs-keyword">public</span> static <span class="hljs-keyword">final</span> String APPLICATION_ID = <span class="hljs-string">"com.example.app"</span>;
<span class="hljs-keyword">public</span> static <span class="hljs-keyword">final</span> String HOST_URL=<span class="hljs-string">"https://staging.api.com"</span>;
<span class="hljs-keyword">public</span> static <span class="hljs-keyword">final</span> String SOME_KEY = <span class="hljs-string">"staging_key"</span>;
}
</code></pre>
<p>If we can somehow tweak this <code>BuildConfig.java</code> to accommodate the configurations for all supporting <code>buildFlavors</code> and environments, this will solve the problem for us. Unfortunately, <code>BuildConfig.java</code> is generated at compile time and cannot be dynamically changed once the build process has been completed. What if we don’t have to change this dynamically and still be able to hold the configurations for different <code>productFlavor</code>. Let’s build on top of this idea.</p>
<h4 id="heading-injecting-build-configuration">Injecting build configuration 🏗</h4>
<p>We now understand that <code>BuildConfig.java</code> holds all the values according to the <code>key</code> type defined under <code>buildConfigField</code>. For <code>String</code> key as <code>HOST_URL</code>, it generated <code>public static final String HOST_URL</code> with its subsequent value. To allow for switching environments from one build, we would like to have all the configurations(prod/staging, etc.) available under our <code>BuildConfig.java</code>.</p>
<p><strong>Mapping Build Flavor with Configurations</strong></p>
<p>We will set up all the <code>buildFlavors</code> and their corresponding environment configurations in a way such that the auto-generated<code>BuildConfig.java</code>contains the following:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">public</span> static <span class="hljs-keyword">final</span> java.util.Map&lt;String,String&gt; PROD_MAP = 
new java.util.HashMap() {{put(<span class="hljs-string">"HOST_URL"</span>,<span class="hljs-string">"https://prod.api.com"</span>); put(<span class="hljs-string">"SOME_KEY"</span>,<span class="hljs-string">"prod_key"</span>)}};

<span class="hljs-keyword">public</span> static <span class="hljs-keyword">final</span> java.util.Map&lt;String,String&gt; STAGING_MAP = 
new java.util.HashMap() {{put(<span class="hljs-string">"HOST_URL"</span>,<span class="hljs-string">"https://staging.api.com"</span>); put(<span class="hljs-string">"SOME_KEY"</span>,<span class="hljs-string">"staging_key"</span>)}};

<span class="hljs-keyword">public</span> static <span class="hljs-keyword">final</span> java.util.ArrayList SET_OF_FLAVORS = 
new java.util.ArrayList() {{add(<span class="hljs-string">"prod"</span>);add(<span class="hljs-string">"staging"</span>);}};
</code></pre>
<p>If we <em>can</em> generate the <code>PROD_MAP</code> and <code>STAGING_MAP</code>along with a <code>set</code> of supported flavors, our app will have all the information available to switch the configuration at runtime. The concept is also driven by a standard code generator like <a target="_blank" href="https://github.com/square/kotlinpoet">KotlinPoet</a>.</p>
<p>Before we do this step, we should also do housekeeping of our <code>build.gradle.kts</code> to move all the build configurations to one place for easy maintenance. We will create <code>Config.kt</code> inside the <code>buildSrc</code> folder for the same.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="3b75bc7f269088d42272d149260dcad0"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/reactivedroid/3b75bc7f269088d42272d149260dcad0" class="embed-card">https://gist.github.com/reactivedroid/3b75bc7f269088d42272d149260dcad0</a></div><p> </p>
<p>Once we have segregated the build configurations, we will now tweak our <code>build.gradle.kts</code> file to auto-generate the subsequent flavor <code>map</code> and <code>set</code> on <code>BuildConfig.java</code>.</p>
<ul>
<li>Under <code>defaultConfig</code>, we will ensure that all our <em>Keys</em> are available on <code>BuildConfig</code> file.</li>
</ul>
<pre><code class="lang-kotlin">Config.keyList.forEach { key -&gt;
buildConfigField(“String”, “KEY_$key”, “\”$key\””) 
}
<span class="hljs-comment">// This generates the keys and writes on BuildConfig</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BuildConfig</span> </span>{
  <span class="hljs-keyword">public</span> static <span class="hljs-keyword">final</span> String KEY_HOST_URL = <span class="hljs-string">"HOST_URL"</span>;
  <span class="hljs-keyword">public</span> static <span class="hljs-keyword">final</span> String KEY_SOME_KEY = <span class="hljs-string">"SOME_KEY"</span>;
}
</code></pre>
<ul>
<li>Next, to have the configurations available for each <code>productFlavor</code><em>,</em> we will change our <code>build.gradle.kts</code> to generate <code>&lt;flavor&gt;_MAP</code> on <code>BuildConfig.java</code>.</li>
</ul>
<pre><code class="lang-kotlin">applicationVariants.all {
 defaultFlavors.forEach { value -&gt; 
  buildConfigField(
  <span class="hljs-string">"java.util.Map&lt;String,String&gt;"</span>,  <span class="hljs-comment">// type </span>
  <span class="hljs-string">"<span class="hljs-subst">${value.toUpperCase()}</span>_MAP"</span>, <span class="hljs-comment">// name</span>
  variantFields(value)      <span class="hljs-comment">// value          </span>
  )            
 }
}
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">variantFields</span><span class="hljs-params">(flavor: <span class="hljs-type">String</span>)</span></span>: String {
    <span class="hljs-keyword">val</span> fields = variantFieldMap[flavor] <span class="hljs-comment">// Defined in Config.kt</span>
    <span class="hljs-keyword">val</span> fieldsBuilder = StringBuilder(<span class="hljs-string">""</span>)
    fields!!.forEach { entry -&gt;
   fieldsBuilder.append(<span class="hljs-string">"put(\"<span class="hljs-subst">${entry.key}</span>\",\"<span class="hljs-subst">${entry.value}</span>\");"</span>)
    }
    <span class="hljs-keyword">return</span> <span class="hljs-string">"new java.util.HashMap() {{<span class="hljs-variable">$fieldsBuilder</span>}}"</span>
}
</code></pre>
<blockquote>
<p>Since <a target="_blank" href="https://developer.android.com/reference/tools/gradle-api/4.1/com/android/build/api/variant/BuildConfigField"><code>buildConfigField</code></a>) only takes <code>String</code> as the value type that can be written on <code>BuildConfig</code>, we have provided the <code>type</code>explicitly as <code>java.util.Map&lt;String, String&gt;</code> and the value as the build configurations map.</p>
</blockquote>
<p>This will write <code>PROD_MAP</code> and <code>STAGING_MAP</code> on <code>BuildConfig</code> like this:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">public</span> static <span class="hljs-keyword">final</span> java.util.Map&lt;String,String&gt; PROD_MAP = 
new java.util.HashMap() {{put(<span class="hljs-string">"HOST_URL"</span>,<span class="hljs-string">"https://prod.api.com"</span>); put(<span class="hljs-string">"SOME_KEY"</span>,<span class="hljs-string">"prod_key"</span>)}};
<span class="hljs-keyword">public</span> static <span class="hljs-keyword">final</span> java.util.Map&lt;String,String&gt; STAGING_MAP = 
new java.util.HashMap() {{put(<span class="hljs-string">"HOST_URL"</span>,<span class="hljs-string">"https://staging.api.com"</span>); put(<span class="hljs-string">"SOME_KEY"</span>,<span class="hljs-string">"staging_key"</span>)}};
</code></pre>
<ul>
<li>Lastly, we will also write the <code>productFlavors</code> on <code>BuildConfig</code> which app will support. This will help in showing the options to the end-user in switching the environment.</li>
</ul>
<pre><code class="lang-kotlin">applicationVariants.all {
 buildConfigField(<span class="hljs-string">"java.util.ArrayList&lt;String&gt;"</span>, <span class="hljs-string">"SET_OF_FLAVORS"</span>, getFlavorList())
}
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getFlavorList</span><span class="hljs-params">()</span></span>: String {
    <span class="hljs-keyword">val</span> flavorBuilder = StringBuilder()
    defaultFlavors.forEach {
        flavorBuilder.append(<span class="hljs-string">"add(\"<span class="hljs-variable">$it</span>\");"</span>)
    }
    <span class="hljs-keyword">return</span> <span class="hljs-string">"new java.util.ArrayList() {{<span class="hljs-variable">$flavorBuilder</span>}}"</span>
}
</code></pre>
<p>This will write the <code>list</code> of flavors on <code>BuildConfig</code>.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">public</span> static <span class="hljs-keyword">final</span> java.util.ArrayList&lt;String&gt; SET_OF_FLAVORS = 
new java.util.ArrayList() {{add(<span class="hljs-string">"prod"</span>);add(<span class="hljs-string">"staging"</span>);}};
</code></pre>
<p>Once we have executed the above steps, our apps <code>build.gradle.kts</code> should look something like this:</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="2a7ba19c6af18fbecfacfe4d38661330"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/reactivedroid/2a7ba19c6af18fbecfacfe4d38661330" class="embed-card">https://gist.github.com/reactivedroid/2a7ba19c6af18fbecfacfe4d38661330</a></div><p> </p>
<blockquote>
<p>To ensure the security of our app, for any buildType of <code>release</code> variant like <code>prodRelease</code>, we will not write other flavor configurations on <code>BuildConfig</code> and just create the <code>RELEASE_MAP</code> (line:13 on above GIST)</p>
</blockquote>
<h4 id="heading-accessing-the-buildconfig">Accessing the BuildConfig 🔓</h4>
<p>We have everything written on <code>BuildConfig</code> as required to develop the flow for dynamically switching environments. Let’s connect the final dots.</p>
<p>We will create an <a target="_blank" href="https://en.wikipedia.org/wiki/Abstraction_%28computer_science%29"><em>abstraction</em></a> over <code>BuildConfig</code> for accessing their values. If you are using <a target="_blank" href="https://en.wikipedia.org/wiki/Dependency_injection">Dependency Injection</a> via <a target="_blank" href="https://dagger.dev/">Dagger</a> or <a target="_blank" href="https://developer.android.com/training/dependency-injection/hilt-android">HILT</a>, it will help in keeping your <a target="_blank" href="https://en.wikipedia.org/wiki/Class_%28computer_programming%29#Abstract_and_Concrete">concrete classes</a> clean enhancing the Unit Testing. Here’s what our <code>BuildConfigProvider</code> will look like this:</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="28d7f4b8b93499ebad96dd7a9c331c8d"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/reactivedroid/28d7f4b8b93499ebad96dd7a9c331c8d" class="embed-card">https://gist.github.com/reactivedroid/28d7f4b8b93499ebad96dd7a9c331c8d</a></div><p> </p>
<p><code>BuildConfigProviderImpl</code> will have the detailed implementation of all the APIs exposed via <code>BuildConfigProvider</code>.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="efd28badd08dac80dd46dc31adee80a9"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/reactivedroid/efd28badd08dac80dd46dc31adee80a9" class="embed-card">https://gist.github.com/reactivedroid/efd28badd08dac80dd46dc31adee80a9</a></div><p> </p>
<p>Now, all we will need to do is to define a <a target="_blank" href="https://developer.android.com/training/dependency-injection/hilt-android#define-bindings"><em>binding</em></a> for <code>BuildConfigProvider</code> such that <a target="_blank" href="https://dagger.dev/">Dagger</a> or <a target="_blank" href="https://developer.android.com/training/dependency-injection/hilt-android">HILT</a> knows how to provide the instance of this abstraction. Once this is set up, we will be able to <a target="_blank" href="https://developer.android.com/training/dependency-injection/hilt-android#inject-interfaces"><em>inject</em></a> <code>BuildConfigProvider</code> as a dependency wherever required.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Module</span>
<span class="hljs-meta">@InstallIn(SingletonComponent::class)</span>
<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppBindingModule</span> </span>{
 <span class="hljs-meta">@Binds</span>
 <span class="hljs-keyword">abstract</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">provideBuildConfigProvider</span><span class="hljs-params">(buildConfigProvider: <span class="hljs-type">BuildConfigProviderImpl</span>)</span></span>: BuildConfigProvider
}
</code></pre>
<h4 id="heading-change-environment-wrt-flavor">Change Environment w.r.t Flavor 🚀</h4>
<p>With the above implementation in place, we were able to provide the environment-switching capability right from within the <a target="_blank" href="https://play.google.com/store/apps/details?id=com.citrus.citruspay">LazyPay</a> app.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697539487489/e125dd36-0379-4d86-9e8f-27f6b2214847.png" alt class="image--center mx-auto" /></p>
<p><a target="_blank" href="https://play.google.com/store/apps/details?id=com.citrus.citruspay">LazyPay</a> debug app showcasing supported environments. Build using <a target="_blank" href="https://developer.android.com/jetpack/compose">Jetpack Compose</a> ❤️</p>
<h4 id="heading-add-on-custom-domain-support">Add-On: Custom Domain Support 🚀 💯</h4>
<p>While we have developed the environment switching feature, there is one feature missing in our implementation and that is to support custom domain configuration. This also came as huge demand from our Backend and <a target="_blank" href="https://samnewman.io/patterns/architectural/bff/">BFF</a> Developers as they wanted to dev test their APIs from their local setup. Keep reading to know how we built it.</p>
<p>Until now, we have provided support for all the environments where we have 1:1 mapping of <code>productFlavor</code> and its <code>build</code> environment(<code>HOST_URL</code>). We will now expand our idea to provide custom domain support.</p>
<ul>
<li><p>Since only <code>HOST_URL</code> will have to change, we will fix the <code>productFlavor</code> where other configurations remain the same and only <code>HOST_URL</code> can be edited. We will be configuring it on our <code>staging</code> flavor.</p>
</li>
<li><p>We will tweak our <code>BuildConfigProvider</code> to expose the setter/getter for changing the <code>HOST_URL</code>.</p>
<pre><code class="lang-kotlin">  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BuildConfigProviderImpl</span> : <span class="hljs-type">BuildConfigProvider {</span></span>
      ...
   <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setHostUrl</span><span class="hljs-params">(baseUrl: <span class="hljs-type">String</span>)</span></span> {
    <span class="hljs-comment">// store in SharedPreferences</span>
   }
   <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getHostUrl</span><span class="hljs-params">()</span></span>: String {
       <span class="hljs-keyword">val</span> baseUrl = <span class="hljs-comment">// get from SharedPreferences else return empty</span>
       <span class="hljs-keyword">return</span> baseUrl ?: getValue(BuildConfig.KEY_CS_JAVA_URL)
   }
  }
</code></pre>
</li>
<li><p>Unfortunately, the app will start throwing the below error when the domain is changed.</p>
<pre><code class="lang-kotlin">  java.net.UnknownServiceException: CLEARTEXT communication to &lt;custom_domain&gt; not permitted <span class="hljs-keyword">by</span> network security policy
</code></pre>
</li>
<li><p><em>Solving java.net.UnknownServiceException</em>: As all our <code>productFlavor</code> with the <code>HOST_URL</code> like <code>https://prod.api.com</code>will go via secure connections, <code>[clearTextTrafficPermitted](https://developer.android.com/training/articles/security-config#CleartextTrafficPermitted)</code> is always set to <code>false</code> by default. This will avoid fraudulent and insecure connections to our app. But, custom domain setup will require us to tweak the <code>[network-security-config](https://developer.android.com/training/articles/security-config)</code> to allow for insecure connections. We will create separate <code>network-security-config</code> files for different <code>buildType</code>. Our <code>debug</code> variants will allow for <code>[clearTextTrafficPermitted](https://developer.android.com/training/articles/security-config#CleartextTrafficPermitted)</code> while the release won’t.</p>
<pre><code class="lang-kotlin">  <span class="hljs-comment">// Create debug folder and place network-security-config under there</span>
  &lt;network-security-config&gt;
      &lt;base-config cleartextTrafficPermitted=<span class="hljs-string">"true"</span>/&gt;
      &lt;debug-overrides&gt;
          &lt;trust-anchors&gt;
              &lt;certificates src=<span class="hljs-string">"system"</span> /&gt;
              &lt;certificates src=<span class="hljs-string">"user"</span> /&gt;
          &lt;/trust-anchors&gt;
      &lt;/debug-overrides&gt;
  &lt;/network-security-config&gt;
</code></pre>
</li>
</ul>
<p>At LazyPay, we provided custom domain support on our <code>sbox</code> environment like this.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697539488926/9edd7200-b7d2-4a3f-b856-833d0b38507c.png" alt class="image--center mx-auto" /></p>
<p><a target="_blank" href="https://play.google.com/store/apps/details?id=com.citrus.citruspay">LazyPay</a> debug app showcasing custom domain support. Build using <a target="_blank" href="https://developer.android.com/jetpack/compose">Jetpack Compose</a> ❤️</p>
<blockquote>
<p>Once a user attempts to switch the domain/environment on app, it is recommended to clear any app cache restarting the app as a fresh launch.</p>
</blockquote>
<h4 id="heading-add-on-obfuscation-on-release-variant">Add-On: Obfuscation on Release Variant 🚀 💯</h4>
<p>If we like to have the above feature built on a variant where <a target="_blank" href="https://developer.android.com/studio/build/shrink-code#enable">obfuscation and minification</a> run via Proguard typically <code>release</code> variant, our apps will crash due to code obfuscation. To fix this, we will add the following rule on <code>proguard-rules.pro</code>:</p>
<pre><code class="lang-kotlin"># Not obfuscating config-maps of BuildConfig <span class="hljs-keyword">as</span> we are accessing them via reflections
-keepclassmembers <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">com</span>.<span class="hljs-title">citrus</span>.<span class="hljs-title">citruspay</span>.<span class="hljs-title">BuildConfig</span> </span>{
    <span class="hljs-keyword">public</span> static <span class="hljs-keyword">final</span> java.util.Map PROD_MAP;
    <span class="hljs-keyword">public</span> static <span class="hljs-keyword">final</span> java.util.Map STAGING_MAP;
}
</code></pre>
<h3 id="heading-conclusion">Conclusion ✅</h3>
<p>We have provided all our learnings in building a feature that directly enhances the developer's productivity and experience. Time saved in generating different build variants is time gained for faster development and testing cycles.</p>
<p>As we gear up for a new age in financial technology, we are also striving hard to prepare our apps built for India scale following <a target="_blank" href="https://developer.android.com/series/mad-skills">Modern Android Development</a>, writing UI in <a target="_blank" href="https://developer.android.com/jetpack/compose">Jetpack Compose</a>, Unit Tests, and much more. At <a target="_blank" href="https://play.google.com/store/apps/details?id=com.citrus.citruspay">LazyPay</a>, we are working every day to improve not only our products but also excel in terms of how we engineer them for scale. If this excites you, come join our Experience Engineering team at PayU Credit. We are <a target="_blank" href="https://jobs.eu.lever.co/payu">hiring</a> across all domains.</p>
<p><em>PS: Next blog will talk about how our iOS engineering team developed a similar feature to improve their productivity. Stay tuned.</em> 🙌</p>
]]></content:encoded></item><item><title><![CDATA[Improving CI/CD pipeline for Android via Fastlane and GitHub Actions]]></title><description><![CDATA[CI/CD bridges the gaps between development and operation activities and teams by enforcing automation in the building, testing and deployment of applications. — Wikipedia

CI/CD is not new, and it has been at the forefront of adopting good software e...]]></description><link>https://blogs.reactivedroid.com/improving-ci-cd-pipeline-for-android-via-fastlane-and-github-actions-a635162d2c53</link><guid isPermaLink="true">https://blogs.reactivedroid.com/improving-ci-cd-pipeline-for-android-via-fastlane-and-github-actions-a635162d2c53</guid><category><![CDATA[Android]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[github-actions]]></category><category><![CDATA[release management]]></category><category><![CDATA[deployment]]></category><dc:creator><![CDATA[Ashwini Kumar]]></dc:creator><pubDate>Fri, 05 Feb 2021 23:38:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/qjvuAC1z91I/upload/2e50addcf44b2c6b731a5a32f91affb8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>CI/CD bridges the gaps between development and operation activities and teams by enforcing automation in the building, testing and deployment of applications. — <a target="_blank" href="https://en.wikipedia.org/wiki/CI/CD">Wikipedia</a></p>
</blockquote>
<p>CI/CD is not new, and it has been at the forefront of adopting good software engineering practices. While it is vital for all, it becomes even more critical for companies that are just getting started, typically startups. They follow the infamous quote of moving fast and breaking things. When the system begins becoming unstable, the need for proper automation(development/testing/deployment) kicks in while still maintaining development and release speeds. It eventually boils down to adopting a proper CI/CD pipeline right from the beginning and improving upon it as the underlying services/apps scale. This article provides an overview of the core concepts for building a CI/CD pipeline for Android via <a target="_blank" href="https://fastlane.tools/">Fastlane</a> and <a target="_blank" href="https://docs.github.com/en/free-pro-team@latest/actions">GitHub Actions</a>.</p>
<p>Sit back and onboard the build automation rocket 🚀</p>
<h3 id="heading-building-cicd-pipeline">Building CI/CD pipeline 🔨</h3>
<blockquote>
<p>You can skip this section if you do not have any private libraries.</p>
</blockquote>
<h4 id="heading-preface-migration-from-jfrogs-artifactoryhttpsjfrogcomartifactory-to-github-packageshttpsgithubcomfeaturespackages">Preface: Migration from <a target="_blank" href="https://jfrog.com/artifactory/">JFrog’s Artifactory</a> to <a target="_blank" href="https://github.com/features/packages">GitHub Packages</a></h4>
<p>If your Android app relies on several in-house libraries, you would be probably using <a target="_blank" href="https://jfrog.com/artifactory/">JFrog’s Artifactory</a> to manage your repositories served via Maven. While Artifactory is incredible in its own way, you lose out on the principle of a single source of truth. That typically means that if your code repository is on GitHub, you will ideally want all the related packages also available in the same place. This helps in better maintenance and visibility. When you start setting up a CI/CD pipeline on <a target="_blank" href="https://github.com/features/actions">GitHub action</a>, you will face the first blocker as the packages are hosted privately on Artifactory via intranet requiring a VPN connection. Here are some out-of-the-box solutions:</p>
<ol>
<li><p><strong>Go Public</strong>  -  You can make Artifactory publicly accessible, obviously under the authenticated environment. If the packages are private, why would you want them to be hosted publicly? Also, isn’t it still defeating the idea of a single source of truth?</p>
</li>
<li><p><strong>Use</strong> <a target="_blank" href="https://docs.github.com/en/free-pro-team@latest/actions/hosting-your-own-runners/about-self-hosted-runners"><strong>Self-Hosted Runners</strong></a> -  Github also provides the power to use self-hosted runners to customize the environment according to your workflow needs. Since the environment will be hosted and created according to your needs, you can easily pull out the private libraries while building the app on Github action. This sounds good but fails due to the massive efforts it would require to build and stabilize while still investing time in the maintenance of it.</p>
</li>
</ol>
<blockquote>
<p>“We can not solve our problems with the same level of thinking that created them”<br />― <strong>Albert Einstein</strong></p>
</blockquote>
<p>As mentioned earlier, the aim of the pipeline is to have a cleaner separation of concern with clearer visibility having GitHub as the single source of truth (code, wiki, versioning, changelog, actions, releases, etc.) for your repositories. Proper version/release management, faster deployment, and deeper integration with GitHub services will help in running a smoother pipeline and enhance it for future needs.</p>
<p>If you are already publishing your libraries to Artifactory via Maven, it would require little effort in <code>build.gradle</code> file to start publishing <code>.aar</code> files on GitHub. You can maintain a<code>github.properties</code> that contains <code>github_token</code> and <code>github_username</code> to give the capability to publish locally for fallback. The <code>pom</code> file for the <code>.aar</code> packages will also be published under GitHub packages.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="bb4cf4bf41f304883667ad694194dbf2"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/reactivedroid/bb4cf4bf41f304883667ad694194dbf2" class="embed-card">https://gist.github.com/reactivedroid/bb4cf4bf41f304883667ad694194dbf2</a></div><p> </p>
<p>Using <code>./gradlew publish</code> locally, you can test your integration and find that the libraries are properly getting published on GitHub packages under your targeted repository.</p>
<p>Minimal changes are required in your Android project’s <code>build.gradle</code> file to <em>consume</em> the library.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="b4b13265a36a116625cb97d92f58e758"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/reactivedroid/b4b13265a36a116625cb97d92f58e758" class="embed-card">https://gist.github.com/reactivedroid/b4b13265a36a116625cb97d92f58e758</a></div><p> </p>
<h4 id="heading-github-action-and-fastlane-working-together">Github Action and Fastlane working together</h4>
<p>Once you have the in-house libraries being pulled in via GitHub, you can start creating the workflow for your Android app on GitHub actions. You can have two different strategies depending upon pushes on the target branches(<code>development</code> &amp; <code>master</code>) These are aimed at not only removing the manual intervention altogether by having continuous integration embedded deep inside the workflow but also focussing on continuous deployment by automating the overall release process right from building the bundle and uploading to the Play Store. Let's discuss the important steps in the workflow:</p>
<ul>
<li><p><strong>No hardcoding</strong>  - Every variable used in the workflow, is made to come via GitHub secrets. For creating a release build, you can first encrypt your <code>.jks</code> and <code>release.properties</code> files containing signing information and then store them on GitHub secrets.</p>
<pre><code class="lang-bash">  // Encryption
  gpg --armor --symmetric release.properties
  // Decryption 
  gpg -d --passphrase <span class="hljs-string">"&lt;Password used for encryption stored as a GitHub secret&gt;"</span> --batch release.properties.asc
</code></pre>
</li>
<li><p>Similarly, to upload your release on the Play Store, you can encrypt your Google service account file and then save it as a secret.</p>
<pre><code class="lang-yaml">  <span class="hljs-comment"># Decrypt release keystore and release properties file from github secrets</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Decrypt</span> <span class="hljs-string">files</span>
    <span class="hljs-attr">id:</span> <span class="hljs-string">decrypt_files</span>
    <span class="hljs-attr">run:</span> <span class="hljs-string">|
    # Creating encrypted keystore file from secret saved on GitHub
      echo "${{ secrets.YOUR_APP_RELEASE_STORE }}" &gt; customer.jks.asc
    # Decrypting keystore from passphrase(saved as secret) used earlier for encryption
      gpg -d --passphrase "${{ secrets.YOUR_APP_RELEASE_PASSWORD }}" --batch customer.jks.asc &gt; config/customer.jks
    # Creating encrypted release.properties file from saved secret.
      echo "${{ secrets.YOUR_APP_RELEASE_PROPERTIES}}" &gt; release.properties.asc
    # Decrypting release.properties.asc with passphrase(saved as secret) used earlier for encryption
      gpg -d --passphrase "${{ secrets.YOUR_APP_RELEASE_PASSWORD }}" --batch release.properties.asc &gt; config/release.properties
    # Creating encrypted play service account file from saved secret.
      echo "${{ secrets.YOUR_PLAY_RELEASE_SERVICE_ACCOUNT}}" &gt; &lt;your_play_service_account_filename&gt;.json.asc
    # Decrypting play service account with passphrase(saved as secret) used earlier for encryption
      gpg -d --passphrase "${{ secrets.YOUR_APP_RELEASE_PASSWORD }}" --batch &lt;your_play_service_account_filename&gt;.json.asc &gt; &lt;your_play_service_account_filename&gt;.json
    # Creating tester-groups file used for publishing on firebase app distribution
      echo "android,qa" &gt; tester-groups.txt
    # Creating empty github.properties to avoid the build failure since our local builds depend on it
      echo " " &gt; github.properties</span>
</code></pre>
</li>
<li><p><strong>Setting up Fastlane and Firebase tools —</strong> If your app is already using <a target="_blank" href="https://fastlane.tools/">Fastlane</a>, you can run Fastlane commands manually to trigger a build and send it on <a target="_blank" href="https://slack.com/intl/en-in/">Slack</a> and <a target="_blank" href="https://firebase.google.com/docs/app-distribution">Firebase App distribution</a>. It is about auto-triggering those commands from your workflow and the rest will be taken care of by Fastlane itself. Slack is used to communicate the status of our build workflow and send <code>.apk</code>or <code>.aab</code> to keep the concerned folks notified in real-time by using different GitHub Actions described further in the article.</p>
</li>
</ul>
<blockquote>
<p>Firebase App Distribution enables you to instantly manage and deliver pre-release versions of your app with trusted testers in order to get feedback and find issues ahead of releasing to production.<br />You can now <a target="_blank" href="https://firebase.googleblog.com/2021/05/app-distribution-adds-support-to-android-app-bundles.html">distribute android app bundle</a> too from Firebase App Distribution 🎉</p>
</blockquote>
<ul>
<li><p><strong>Faster workflow  -</strong> <em>Set up Gradle</em>, <em>Download the dependencies</em>, <em>Set up Ruby</em>, <em>Install Firebase Tools</em>, <em>Set up Fastlane</em>, <em>Install Bundler,</em> etc are the steps that get executed for every workflow trigger. This will affect your GitHub Actions in billing minutes leading to higher payments. You can use <code>[actions/cache@v2](https://github.com/actions/cache)</code> to have a faster workflow execution time.</p>
<p>  %[https://gist.github.com/reactivedroid/c7e3bf75da50a5b532439c5bbc64f1b3] </p>
</li>
</ul>
<p>You can use <em>Gradle cache</em> and <em>Gems cache</em> to avoid downloading the jars and gems if there is no change in the dependencies. It is important to create a cache key in such a way that it can be invalidated when required thus avoiding stale builds. For Gradle cache, checksum acts as the cache key which is calculated by running the checksum script( <code>[checksum.sh](https://github.com/chrisbanes/tivi/blob/main/checksum.sh)</code>)[Thanks to <a target="_blank" href="https://medium.com/u/9303277cb6db">Chris Banes</a>]. For gems, the <code>hashFiles(Gemfile.lock)</code> acts as the cache key. Once there is a Gradle/Gems cache hit, the setup time will drastically reduce giving faster workflow runs.</p>
<ul>
<li><p><strong>Running UI/Unit tests —</strong> UI/Unit tests play a vital role in making the app robust. It helps to track any broken features upfront when a huge refactoring/re-architecture task takes place. If the Unit/UI tests are not working as expected, you can fail the CI/CD pipeline to keep the tests always up-to-date with the newer changes. Once the tests have been executed successfully, you can <a target="_blank" href="https://github.com/actions/upload-artifact">upload</a> the test reports and results on the workflow for future analysis.</p>
<p>  %[https://gist.github.com/reactivedroid/abf7f60c34c5f821df9658d55543cff1] </p>
</li>
<li><p><strong>Creating release builds—</strong> Once you have made the necessary setup to run Fastlane via <code>bundle</code>, you can execute <code>lane</code> based on the branch push type <code>development</code>&amp; <code>master</code> to create debug and release builds respectively. You can club all the steps under one workflow separated <code>if: github.ref == ‘refs/heads/master’</code>to have your development and release builds go seamlessly. Let’s break this down even further.</p>
</li>
</ul>
<ol>
<li><p><em>Distributing app bundle via Fastlane</em> - You can write a <code>lane</code> which will <code>bundleProdRelease</code> and then <code>push</code> the release tag on the<code>master</code> branch. This will then publish the build on the Slack channel and <a target="_blank" href="https://firebase.google.com/docs/app-distribution">Firebase App Distribution</a>. This is how the <code>lane</code> looks like in <code>fastfile</code> </p>
<p> %[https://gist.github.com/reactivedroid/0dbbe11ebec06a2c671543dc78e19d31] </p>
</li>
<li><p><em>Lane Options on Fastlane -</em> Fastlane needs some values to run the <code>distribute_bundle</code> lane. You can store the required values as secrets on GitHub and then inject them into Fastlane as <a target="_blank" href="https://docs.fastlane.tools/advanced/lanes/">lane options</a> to be used later.</p>
<p> %[https://gist.github.com/reactivedroid/c8c513a5643a24cdd508482b798d186e] </p>
</li>
<li><p><em>Creating GitHub release from Workflow  -</em> Once the release tag is pushed on Git via <code>build_prod_release</code> step above, you can save it as <code>tag_name</code> and use it to create our GitHub release. To know what actually went in the release, you can check-in <code>changelog.md</code>or <code>release_notes.txt</code> into version control. This file will be used to push releases both on Firebase app distribution and GitHub. You can also use the checked-in release notes to create your GitHub releases right from workflow while uploading other assets like bundle/apk.</p>
<p> %[https://gist.github.com/reactivedroid/cc1c5e7554c61a49c312fc2e0a3c1e5f] </p>
</li>
<li><p><em>Uploading to the Play Store  -</em> The final piece is automating the deployment process from the workflow. You can create a service account file from <a target="_blank" href="https://console.cloud.google.com/">Google Cloud Console</a>, associate it with Play Store by giving necessary permissions and use it in your workflow to upload on <code>internal</code> track. Once the build is approved by the Play folks, you can promote it to production.</p>
<p> %[https://gist.github.com/reactivedroid/0b238fd16e5da39640f02fd448b352e0] </p>
</li>
<li><p><em>Uploading build outputs and send status</em>  <strong>-</strong> Hope you are still with us :) The final step includes uploading all the build outputs on workflow and then notifying about its status on Slack via <a target="_blank" href="https://github.com/act10ns/slack">action</a>.</p>
<p> %[https://gist.github.com/reactivedroid/6f7f04470bd67a6e8624ebcb6c94539d] </p>
</li>
</ol>
<p>Phew! That was a long read, right 😌. I understand that creating a seamless CI/CD workflow for Android builds can be tricky and it becomes even more complicated when you want to automate each and every step from continuous development to production.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697627409428/2c7ac4d2-a94e-43f3-a5d9-67519ede7fce.webp" alt class="image--center mx-auto" /></p>
<p>For that reason, I have tried my best to provide you with as many details as possible so that setting up CI/CD pipeline should be a fairly easy task going forward and you can reap its benefits in the longer run. Obviously, this smooth setup would not have been viable without the ever-growing open-source contributions ❤️. You can use <a target="_blank" href="https://github.com/act10ns/slack">slack action</a> to get the status of your workflow right into your Slack channel. Sweet, isn’t it!</p>
<h3 id="heading-conclusion">Conclusion</h3>
<p>This is not the only way to create a CI/CD workflow on GitHub action and I am all ears to listen to your creative ideas. I will feel more happy and content if this article helps you in any way possible. Till then, keep learning and sharing. #BetterTogether</p>
]]></content:encoded></item><item><title><![CDATA[Enhanced selfie experience via MLKit]]></title><description><![CDATA[At InCred, we are reinventing the lending businesses by emphasizing and investing more and more in cutting-edge technologies to solve real-world problems. Our aim is to provide a hassle-free lending process with an awesome customer experience. This h...]]></description><link>https://blogs.reactivedroid.com/enhanced-selfie-experience-via-mlkit-ffc52017045b</link><guid isPermaLink="true">https://blogs.reactivedroid.com/enhanced-selfie-experience-via-mlkit-ffc52017045b</guid><category><![CDATA[Android]]></category><category><![CDATA[Firebase]]></category><category><![CDATA[user experience]]></category><category><![CDATA[Machine Learning]]></category><dc:creator><![CDATA[Ashwini Kumar]]></dc:creator><pubDate>Tue, 01 Sep 2020 14:07:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/e7jq0NH9Fbg/upload/721ca6595ace5a3a7d496f692505f722.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>At <a target="_blank" href="https://www.incred.com/">InCred</a>, we are reinventing the lending businesses by emphasizing and investing more and more in cutting-edge technologies to solve real-world problems. Our aim is to provide a hassle-free lending process with an awesome customer experience. This has become more relevant in these tough and challenging times(COVID-19) where the fewer the human interactions are, the safer the world can be. To have zero paperwork in the process, we introduced customer tasks where users can complete any task like Taking a selfie, Signing the agreement, Submitting documents, NACH registration, etc by themselves without requiring any human interaction. <code>Taking a selfie</code> is the most important step in our loan lending business because an unclear picture can lead to the delay or rejection of the loan application. This article will tell about how we are utilizing <a target="_blank" href="https://developers.google.com/ml-kit/vision/face-detection">MLKit</a> to improve the Selfie experience of our users.</p>
<h3 id="heading-face-detection-via-mlkithttpsdevelopersgooglecomml-kitvisionface-detection"><a target="_blank" href="https://developers.google.com/ml-kit/vision/face-detection">Face Detection via MLKit</a></h3>
<p>When we started on our journey to improve the Selfie experience, the aim was very simple: <em>Our customers should be able to capture their Selfies clearly for faster loan processing</em>. So, we utilised the power of machine-learning mainly <a target="_blank" href="https://developers.google.com/ml-kit/vision/face-detection/android">Face detection MLKit</a>(earlier via Firebase) to solve just that. We went with the <a target="_blank" href="https://developers.google.com/ml-kit/vision/face-detection/android">unbundled face model approach</a> for MLKit as the bundled one would increase the app size by 16MB. It pulls in the face models lazily when the app is being initialized and opened for the first time.</p>
<pre><code class="lang-xml">// Add this in your AndroidManifest.xml to automatically download the face model from the Play Store after the app is installed
<span class="hljs-tag">&lt;<span class="hljs-name">meta-data</span>
<span class="hljs-attr">android:name</span>=<span class="hljs-string">"com.google.mlkit.vision.DEPENDENCIES"</span>
<span class="hljs-attr">android:value</span>=<span class="hljs-string">"face"</span> /&gt;</span>
</code></pre>
<h4 id="heading-setting-up-camera">Setting up Camera 📸</h4>
<p>The choice of camera library decides the efforts of implementation. Until <a target="_blank" href="https://developer.android.com/training/camerax">CameraX</a> becomes stable, we would want to use the library that provides easy-to-use APIs that are compatible with different Android OS, and then we found <a target="_blank" href="https://github.com/natario1/CameraView">CameraView</a>. <a target="_blank" href="https://github.com/natario1/CameraView">CameraView</a> is well-documented and powered by Camera1(&lt;API 21) and Camera2(≥API 21) engines making it fast &amp; reliable. It has all the features, we need, the most important being <a target="_blank" href="https://natario1.github.io/CameraView/docs/frame-processing">Frame Processing Support</a>. It also offers support on <code>LifeCycleOwner</code> where the <code>CameraView</code> handles the <code>LifeCycle</code> event on its own, mainly asking for permission in <code>onResume</code>, cleaning frame processors &amp; listeners, and destroying <code>cameraView</code>in <code>onDestroy</code> of fragment/activity. By default, <a target="_blank" href="https://github.com/natario1/CameraView">CameraView</a> offloads the Frame processing to the background thread so that these frames can then be consumed by the face detector synchronously.</p>
<pre><code class="lang-kotlin">cameraView.apply {
    setLifecycleOwner(viewLifecycleOwner)
    facing = Facing.FRONT
    addCameraListener(cameraListener())
    addFrameProcessor {
        faceTrackerViewModel.processCameraFrame(it)
        cameraOverlay.setCameraInfo(
            it.size.height,
            it.size.width,
            Facing.FRONT
        )
    }
}
</code></pre>
<blockquote>
<p>It is important that you set the <code>width</code> and <code>height</code> of <code>cameraOverlay</code> to the received frame’s width and height. While testing initially on different devices, the face detected rectangle went away from where the face actually resides in. More details are present in the <a target="_blank" href="https://github.com/natario1/CameraView/issues/802">issue</a> and how we fixed it.</p>
</blockquote>
<h4 id="heading-reactive-frame-processing-for-face-detection">Reactive Frame Processing for Face Detection 📽️</h4>
<p>The frame is then sent to the frame processor via <code>faceTrackerViewModel</code> to detect a face in it.</p>
<pre><code class="lang-kotlin"><span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">processCameraFrame</span><span class="hljs-params">(it: <span class="hljs-type">Frame</span>)</span></span> {
    <span class="hljs-keyword">val</span> byteBuffer = ByteBuffer.wrap(it.getData())
    <span class="hljs-keyword">val</span> frameMetadata =
        FrameMetadata(it.size.width, it.size.height, it.rotationToUser, Facing.FRONT)
    compositeDisposable.add(
        rxFaceDetectionProcessor.process(byteBuffer, frameMetadata)
            .subscribe()
    )
}
</code></pre>
<p>All this is done while keeping the reactive nature of the app intact via <code>RxFaceDetectionProcessor</code> . The <code>ViewModel</code> will pass on the frame to the processor which will process each frame asynchronously off the UI thread.<br /><code>RxFaceDetectionProcessor</code> is the <strong>reactive layer</strong> written over <code>FaceDetectionProcessor</code> that emits face-detection results via <code>FaceDetectionResultListener</code> which are then consumed by <code>faceDetectionResultLiveData = LiveData&lt;List&lt;Face&gt;&gt;</code> . This <code>faceDetectionResultLiveData</code> is observed via the view layer via <code>viewModel</code> to display the <em>rectangular bounding box</em> over the face.</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RxFaceDetectionProcessor</span></span>
<span class="hljs-meta">@Inject</span>
<span class="hljs-keyword">constructor</span>(<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> faceDetectionProcessor: FaceDetectionProcessor) :
    FlowableOnSubscribe&lt;List&lt;Face&gt;&gt;,
    FaceDetectionResultListener {
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> emitter: FlowableEmitter&lt;List&lt;Face&gt;&gt;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> <span class="hljs-keyword">data</span>: ByteBuffer
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> frameMetadata: FrameMetadata
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> faceDetectionResultLiveData: MutableLiveData&lt;List&lt;Face&gt;&gt;

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setFaceDetectionResultLiveData</span><span class="hljs-params">(faceDetectionResultLiveData: <span class="hljs-type">MutableLiveData</span>&lt;<span class="hljs-type">List</span>&lt;<span class="hljs-type">Face</span>&gt;&gt;)</span></span> {
        <span class="hljs-keyword">this</span>.faceDetectionResultLiveData = faceDetectionResultLiveData
    }

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">process</span><span class="hljs-params">(
        <span class="hljs-keyword">data</span>: <span class="hljs-type">ByteBuffer</span>,
        frameMetadata: <span class="hljs-type">FrameMetadata</span>
    )</span></span>: Flowable&lt;List&lt;Face&gt;&gt; {
        <span class="hljs-keyword">this</span>.<span class="hljs-keyword">data</span> = <span class="hljs-keyword">data</span>
        <span class="hljs-keyword">this</span>.frameMetadata = frameMetadata
        <span class="hljs-keyword">return</span> Flowable.create(<span class="hljs-keyword">this</span>, BackpressureStrategy.LATEST)
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">subscribe</span><span class="hljs-params">(emitter: <span class="hljs-type">FlowableEmitter</span>&lt;<span class="hljs-type">List</span>&lt;<span class="hljs-type">Face</span>&gt;&gt;)</span></span> {
        <span class="hljs-keyword">this</span>.emitter = emitter
        faceDetectionProcessor.process(<span class="hljs-keyword">data</span>, frameMetadata, <span class="hljs-keyword">this</span>)
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onSuccess</span><span class="hljs-params">(
        results: <span class="hljs-type">List</span>&lt;<span class="hljs-type">Face</span>&gt;
    )</span></span> {
        faceDetectionResultLiveData.value = results
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onFailure</span><span class="hljs-params">(e: <span class="hljs-type">Exception</span>)</span></span> {
        Timber.d(e)
        faceDetectionResultLiveData.value = emptyList()
    }

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">stop</span><span class="hljs-params">()</span></span> {
        faceDetectionProcessor.stop()
        <span class="hljs-keyword">if</span> (::emitter.isInitialized)
            emitter.setDisposable(Disposables.disposed())
    }
}
</code></pre>
<p><code>FaceDetectionProcessor</code> is the class where actual frame processing happens. This involves creating <a target="_blank" href="https://developers.google.com/android/reference/com/google/mlkit/vision/face/FaceDetector"><code>FaceDetector</code></a> with <a target="_blank" href="https://developers.google.com/android/reference/com/google/mlkit/vision/face/FaceDetectorOptions"><code>FaceDetectorOptions</code></a> to start processing each frame for our use case.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> options = FaceDetectorOptions.Builder()
    .apply {
      setClassificationMode(FaceDetectorOptions
                                       .CLASSIFICATION_MODE_NONE)
       setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_NONE)
       setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
       enableTracking()
    }
    .build()
detector = FaceDetection.getClient(options)
</code></pre>
<p>We wanted the face detection to be as fast as possible without requiring any extra processing on landmarks or classification. Hence, we went for faster performance by limiting the features and not requiring the classification of faces(smiling, eyes open, etc) or any <a target="_blank" href="https://developers.google.com/android/reference/com/google/mlkit/vision/face/FaceLandmark">landmark</a> detection.</p>
<p><strong>Why are the frames getting processed even if the face detector is closed?</strong><br />When we started pushing frames for face detection, we came around with one more blocker issue. We found that even if the face detector was closed and the camera view had been destroyed, the frames kept on getting processed thus affecting the battery performance. Anything which hinders the performance, cannot hit production at any cost. So we sat around to fix the issue. We found that we were feeding more frames to <code>faceDetector</code> than it could actually consume and since <code>faceDetector</code> took time to compute the result for a particular frame, the next frame was getting processed even if the <code>faceDetector</code> was closed because those were already sent in its buffer. Here is the processing logic after the fix.</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FaceDetectionProcessor</span></span>
<span class="hljs-meta">@Inject</span>
<span class="hljs-keyword">constructor</span>() {
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> detector: FaceDetector
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> latestImage: ByteBuffer? = <span class="hljs-literal">null</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> latestImageMetaData: FrameMetadata? = <span class="hljs-literal">null</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> processingImage: ByteBuffer? = <span class="hljs-literal">null</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> processingMetaData: FrameMetadata? = <span class="hljs-literal">null</span>

    <span class="hljs-keyword">init</span> {
        <span class="hljs-comment">// Face detector initialisation here</span>
    }

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">process</span><span class="hljs-params">(
        <span class="hljs-keyword">data</span>: <span class="hljs-type">ByteBuffer</span>,
        frameMetadata: <span class="hljs-type">FrameMetadata</span>,
        detectionResultListener: <span class="hljs-type">FaceDetectionResultListener</span>
    )</span></span> {
        latestImage = <span class="hljs-keyword">data</span>
        latestImageMetaData = frameMetadata
        <span class="hljs-comment">// Process the image only when the last frame processing has been completed</span>
        <span class="hljs-keyword">if</span> (processingImage == <span class="hljs-literal">null</span> &amp;&amp; processingMetaData == <span class="hljs-literal">null</span>) {
            processLatestImage(detectionResultListener)
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">processLatestImage</span><span class="hljs-params">(detectionResultListener: <span class="hljs-type">FaceDetectionResultListener</span>)</span></span> {
        processingImage = latestImage
        processingMetaData = latestImageMetaData
        latestImage = <span class="hljs-literal">null</span>
        latestImageMetaData = <span class="hljs-literal">null</span>
        <span class="hljs-keyword">if</span> (processingImage != <span class="hljs-literal">null</span> &amp;&amp; processingMetaData != <span class="hljs-literal">null</span>) {
            processImage(
                requireNotNull(processingImage),
                requireNotNull(processingMetaData),
                detectionResultListener
            )
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">processImage</span><span class="hljs-params">(
        <span class="hljs-keyword">data</span>: <span class="hljs-type">ByteBuffer</span>,
        frameMetadata: <span class="hljs-type">FrameMetadata</span>,
        detectionResultListener: <span class="hljs-type">FaceDetectionResultListener</span>
    )</span></span> {
        detectInVisionImage(
            InputImage.fromByteBuffer(
                <span class="hljs-keyword">data</span>,
                frameMetadata.width,
                frameMetadata.height,
                frameMetadata.rotation,
                InputImage.IMAGE_FORMAT_NV21
            ),
            detectionResultListener
        )
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">detectInVisionImage</span><span class="hljs-params">(
        image: <span class="hljs-type">InputImage</span>,
        detectionResultListener: <span class="hljs-type">FaceDetectionResultListener</span>
    )</span></span> {
        detector.process(image)
            .addOnSuccessListener {
                detectionResultListener.onSuccess(it)
            }
            .addOnFailureListener {
                detectionResultListener.onFailure(it)
            }.addOnCompleteListener {
                <span class="hljs-comment">// Process the next available frame for face detection</span>
                processLatestImage(detectionResultListener)
            }
    }

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">stop</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">try</span> {
            Timber.d(<span class="hljs-string">"Face detector closed"</span>)
            detector.close()
        } <span class="hljs-keyword">catch</span> (e: IOException) {
            Timber.e(<span class="hljs-string">"Exception thrown while trying to close Face Detector: <span class="hljs-variable">$e</span>"</span>)
        }
    }
}
</code></pre>
<p><img src="https://cdn-images-1.medium.com/max/800/1*MnbhVLYbxot5fthjUs70JA.gif" alt="Amit Randhawa from InCred’s Android team showing the Selfie Experience" class="image--center mx-auto" /></p>
<h3 id="heading-quality-selfies-via-auto-capture">Quality Selfies via Auto Capture 🤳</h3>
<p>We launched our newly developed selfie experience to our users which received very good feedback. At InCred, we always aim to make good, better and then strive for the best. So, we completely enhanced the selfie experience by introducing auto-capture once the face is detected with an utmost quality snapshot. We provided an oval overlay on top of the camera feed and asked our users to have their faces inside it. Once the face was detected, we would auto-capture the selfie. To guide the users properly, we provided real-time feedback like (You are too near to the camera/ You look too far from the camera, etc.) on top of the overlay so that our users could do this task without requiring any manual support.</p>
<h4 id="heading-detecting-the-face-inside-the-oval-and-the-feedback">Detecting the face inside the oval and the feedback</h4>
<p>We added an oval overlay on top of the <a target="_blank" href="https://github.com/googlesamples/mlkit/blob/e949654bef09e97b237c7692349c52ee06e9fd9e/android/material-showcase/app/src/main/java/com/google/mlkit/md/camera/GraphicOverlay.kt">GraphicOverlay</a>. Note that <a target="_blank" href="https://github.com/googlesamples/mlkit/blob/e949654bef09e97b237c7692349c52ee06e9fd9e/android/material-showcase/app/src/main/java/com/google/mlkit/md/camera/GraphicOverlay.kt">GraphicOverlay</a> helped earlier in adding the rectangular bounding box graphic once the face is detected. We were looking at 3 kinds of feedback from the users:</p>
<ul>
<li><p><strong>Face inside oval:</strong> To detect whether the detected face was actually inside the oval, it was just about checking whether the face bounding box was inside the oval dimensions. Here <code>this</code> is the oval dimensions and other parameters for the face bounding box.</p>
<pre><code class="lang-kotlin">  <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">sidesInsideOval</span><span class="hljs-params">(top: <span class="hljs-type">Float</span>, right: <span class="hljs-type">Float</span>, bottom: <span class="hljs-type">Float</span>, left: <span class="hljs-type">Float</span>)</span></span>: <span class="hljs-built_in">Boolean</span> {
      <span class="hljs-keyword">if</span> (top &gt;= <span class="hljs-keyword">this</span>.top &amp;&amp; bottom &lt;= <span class="hljs-keyword">this</span>.bottom &amp;&amp; left &gt;= <span class="hljs-keyword">this</span>.left &amp;&amp; right &lt;= <span class="hljs-keyword">this</span>.right)
          <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
      <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
  }
</code></pre>
</li>
<li><p><strong>The face inside the oval but zoomed out:</strong> The face of the user could be inside the oval but if it is too far from the camera, the selfies captured would not be clear. To verify this case, we compared the vertical distance of the face bounding box with the oval. If it was less than half, we gave the feedback to the user to <em>move closer to the camera.</em></p>
<pre><code class="lang-kotlin">  <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">isFaceZoomedOut</span><span class="hljs-params">(top: <span class="hljs-type">Float</span>, bottom: <span class="hljs-type">Float</span>)</span></span>: <span class="hljs-built_in">Boolean</span> {
      <span class="hljs-keyword">if</span> (((bottom - top) / (<span class="hljs-keyword">this</span>.bottom - <span class="hljs-keyword">this</span>.top)) &lt;= <span class="hljs-number">0.5</span>)
          <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
      <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
  }
</code></pre>
</li>
<li><p><strong>Face zoomed in:</strong> The user could be holding the camera too near to the face which could bring in sub-quality selfies. To verify this, we checked whether any of the coordinates of the face bounding box is greater than the oval dimension or not. If the face was zoomed in, we gave the feedback to the user to <em>move near to the camera.</em></p>
<pre><code class="lang-kotlin">  <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">isFaceZoomedIn</span><span class="hljs-params">(top: <span class="hljs-type">Float</span>, right: <span class="hljs-type">Float</span>, bottom: <span class="hljs-type">Float</span>, left: <span class="hljs-type">Float</span>)</span></span>: <span class="hljs-built_in">Boolean</span> {
      <span class="hljs-keyword">var</span> sidesInside = <span class="hljs-number">0</span>
      <span class="hljs-keyword">if</span> (top &gt;= <span class="hljs-keyword">this</span>.top)
          sidesInside += <span class="hljs-number">1</span>
      <span class="hljs-keyword">if</span> (bottom &lt;= <span class="hljs-keyword">this</span>.bottom)
          sidesInside += <span class="hljs-number">1</span>
      <span class="hljs-keyword">if</span> (left &gt;= <span class="hljs-keyword">this</span>.left)
          sidesInside += <span class="hljs-number">1</span>
      <span class="hljs-keyword">if</span> (right &lt;= <span class="hljs-keyword">this</span>.right)
          sidesInside += <span class="hljs-number">1</span>
      <span class="hljs-keyword">if</span> (sidesInside &lt;= <span class="hljs-number">1</span>)
          <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
      <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
  }
</code></pre>
</li>
</ul>
<p>Once the corner cases had been tested, we experimented with our new selfie experience in conjunction with the old one via <a target="_blank" href="https://firebase.google.com/docs/ab-testing/">Firebase A/B testing</a>. Within a month we found that the oval overlay with the auto-capture feature was performing way better than the former one. This provided confidence in our new feature and then we launched it to all our users.</p>
<p><img src="https://cdn-images-1.medium.com/max/800/1*8fd26kYd2mSrf5wyGESmWw.gif" alt="Amit Randhawa from InCred’s Android team showing the new Selfie Experience" class="image--center mx-auto" /></p>
<h4 id="heading-the-fallback-approach">The fallback approach</h4>
<p>Android is heavily fragmented and with different <a target="_blank" href="https://en.wikipedia.org/wiki/Original_equipment_manufacturer">OEMs</a>, it is more and more difficult to test your feature on all Android devices. This becomes more problematic in a country like India where there is a vast majority of OEMs that customize the behaviour of Android OS according to their needs and requirements. This means that if your feature does not work on some devices, you are basically blocking the user journey in your app. To solve for those users, we provided a fallback approach where if a face was not detected in 10s, we would move to the native camera experience. With this, we were not blocking the user’s loan application journey at any cost while giving us time to fix the issues in the background.</p>
<blockquote>
<p>Always provide the fallback approach for your feature. At the end, you would never want to block your users and then receive bad reviews on Play Store.</p>
</blockquote>
<p>We truly believe that <em>Necessity is the mother of invention.</em> The current tough times are pushing us to re-imagine and build every feature without requiring any human intervention. We are on our path to not only rebuild/revamp our tech stack but also solidify and improve the features that can boost the user experience. If this excites you, we will be more than happy to discuss it.</p>
]]></content:encoded></item><item><title><![CDATA[Uninstall tracking via BigQuery and Airflow]]></title><description><![CDATA[At InCred, our mobile app is an integral part of our lending process. It helps customers complete their loan application, gives them information about their loan, and payment history, allows them to make payments, and stays in touch with InCred. That...]]></description><link>https://blogs.reactivedroid.com/uninstall-tracking-via-bigquery-and-airflow-40eb33d8bedc</link><guid isPermaLink="true">https://blogs.reactivedroid.com/uninstall-tracking-via-bigquery-and-airflow-40eb33d8bedc</guid><category><![CDATA[bigquery]]></category><category><![CDATA[apache-airflow]]></category><category><![CDATA[user retention]]></category><dc:creator><![CDATA[Ashwini Kumar]]></dc:creator><pubDate>Thu, 02 Jul 2020 13:29:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/lirOPuejTbM/upload/90d7c70bca80c657f25e3e5a792bf1ec.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>At <a target="_blank" href="https://www.incred.com">InCred</a>, our <a target="_blank" href="https://play.google.com/store/apps/details?id=com.incred.customer">mobile app</a> is an integral part of our lending process. It helps customers complete their loan application, gives them information about their loan, and payment history, allows them to make payments, and stays in touch with InCred. That's why it is crucial for our business that our customers keep the app installed on their devices. However, sometimes customers may remove the app inadvertently or otherwise. In that situation, they may miss payment reminders and other vital communication related to their loan potentially causing them financial harm. So we wanted to reach out to customers in case of an uninstall quickly. In this article, we will talk about how we are using <a target="_blank" href="https://cloud.google.com/bigquery/">BigQuery</a> to track user uninstalls in conjunction with <a target="_blank" href="https://airflow.apache.org/">Airflow</a> to fire remedial action notifications to our users.</p>
<h3 id="heading-hunt-for-an-api">Hunt for an API</h3>
<p>We would like to build a REST API on top of BigQuery which can tell the app status for a given user on the given month range. At InCred, language is not the barrier where a microservice can be written in Java/GoLang/NodeJs. For this case, we went with NodeJs and used <a target="_blank" href="https://github.com/googleapis/nodejs-bigquery">BigQuery NodeJs client</a> along the way. The API would have the following request body:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"userIds"</span>: [
  &lt;list of user identifiers&gt;
  ],
  <span class="hljs-attr">"startMonth"</span>: <span class="hljs-number">1</span>,
  <span class="hljs-attr">"startYear"</span>: <span class="hljs-number">2020</span>,
  <span class="hljs-attr">"endMonth"</span>: <span class="hljs-number">5</span>,
  <span class="hljs-attr">"endYear"</span>: <span class="hljs-number">2020</span>
}
</code></pre>
<p>For every user, this API should return the status of our app with values ranging from <em>Uninstalled/Installed/Never Installed</em>.</p>
<p>Our Android app fires many events to <a target="_blank" href="https://firebase.google.com/docs/analytics/">Firebase Analytics</a>. These events help in analyzing user behaviour. To gain deeper insights, we have BigQuery integrated into <a target="_blank" href="https://firebase.google.com/docs/analytics/">Firebase analytics</a>. To track uninstalls, we started looking out whether we can explicitly fire any event when the app is getting uninstalled by bundling it with user information. Unfortunately, this was not available and then we came across Firebase <a target="_blank" href="https://support.google.com/firebase/answer/6317485?hl=en">auto-logged events</a>, where one of the events <code>app_remove</code> was specifically meant for that purpose. To define the uniqueness of a user, we send <code>[user_id](https://firebase.google.com/docs/analytics/userid)</code>when the users authenticate via phone. Right, so just querying with <code>app_remove</code> event name will not solve the problem. That wasn’t meant to be!!</p>
<h4 id="heading-what-if-the-user-had-reinstalled-the-app-again">What if the user had reinstalled the app again?</h4>
<p>If we just went with listening to the<code>app_remove</code> event, the user having reinstalled the app, will be shown as <em>Uninstalled.</em> Not the desired result. We also wanted to check if we could find anything unique about the user before logging into our app and then we saw <code>user_pseudo_id</code>. Firebase automatically generates this unique ID within BigQuery for each user. So, we created a map of <code>user_id</code> and <code>user_pseudo_id</code> and then analyzed the uninstalls. This failed too as it started giving false-positive results.</p>
<h4 id="heading-keep-it-simple-sillykiss">Keep-It-Simple-Silly(KISS)</h4>
<p>When the problem becomes complex, it is always good to break it down into sub-problems. Then, solve the sub-problems and connect them all together. For a user in the given month range, we broke this problem into 3 parts:</p>
<ul>
<li><p>We would get the maximum <code>event_timestamp</code> for the <code>app_remove</code> event. This would also club the fact that the user has installed/uninstalled the app multiple times. Since BigQuery shards the table based on date, we used <a target="_blank" href="https://cloud.google.com/bigquery/docs/reference/standard-sql/wildcard-table-reference">wildcard(*)</a> to cover all the tables for a particular month.</p>
<pre><code class="lang-sql">  <span class="hljs-keyword">select</span> <span class="hljs-keyword">max</span>(event_timestamp) <span class="hljs-keyword">as</span> app_remove_timestamp, user_id <span class="hljs-keyword">as</span> user_id <span class="hljs-keyword">from</span> <span class="hljs-string">" + &lt;table_name.yyyymm*&gt; + "</span> <span class="hljs-keyword">where</span> event_name = <span class="hljs-string">'app_remove'</span> <span class="hljs-keyword">and</span> user_id <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>
</code></pre>
</li>
<li><p>We would get the maximum engagement timestamp of a user. This would get the last timestamp when the user engaged with our app.</p>
<pre><code class="lang-sql">  <span class="hljs-keyword">select</span> <span class="hljs-keyword">max</span>(event_timestamp) <span class="hljs-keyword">as</span> max_engage_timestamp, user_id <span class="hljs-keyword">as</span> user_id <span class="hljs-keyword">from</span> <span class="hljs-string">" + &lt;table_name.yyyymm*&gt; + "</span> <span class="hljs-keyword">where</span> user_id <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>
</code></pre>
</li>
<li><p>Both the results would also be run on the same day since the BigQuery intraday table name is a little different. We would combine the last results to give us the bucket for <em>Installed/Uninstalled/Never Installed</em> users. Let’s see how:</p>
<ul>
<li><p>If the <code>app_remove_timestamp</code> is greater than <code>max_engage_timestamp</code>, then the user has <strong><em>Uninstalled</em></strong> the app.</p>
</li>
<li><p>If the <code>app_remove_timestamp</code> is null or less than <code>max_engage_timestamp</code>, then the user has the app <strong><em>Installed</em></strong></p>
</li>
<li><p>If both the timestamps are null, then the user has <strong><em>Never Installed</em></strong> the app in that range.</p>
</li>
</ul>
</li>
</ul>
<blockquote>
<p>Note: All this works for a given month range. We know for sure that when the user has taken a loan from us and then gives an estimated month range on this API to give the desired result.</p>
</blockquote>
<h4 id="heading-why-not-include-a-date-too-to-make-this-api-complete">Why not include a date too to make this API complete?</h4>
<p>With month ranges, we were not able to track the uninstalls every week or in the given time frame, say, we wanted to track from 5th Jan 2020 to 3rd Mar 2020 when we ran a campaign or disbursed loans via some offer. To cater to that, we changed the request body a little to include optional <code>startDay</code> and <code>endDay</code> and then tweaked the earlier solution. With the help of <a target="_blank" href="https://cloud.google.com/bigquery/docs/querying-wildcard-tables#scanning_a_range_of_tables_using_table_suffix"><code>TABLE_SUFFIX</code></a>, we were able to scan the table for the given date range.</p>
<p>For the range of 5th Jan 2020 to 3rd Mar 2020, we used <a target="_blank" href="https://cloud.google.com/bigquery/docs/querying-wildcard-tables#scanning_a_range_of_tables_using_table_suffix"><code>TABLE_SUFFIX</code></a> to get the data set from 5th Jan to 30th Jan, then ran a <a target="_blank" href="https://cloud.google.com/bigquery/docs/reference/standard-sql/wildcard-table-reference">Wildcard(*)</a> for Feb month, and lastly ran a range query with <a target="_blank" href="https://cloud.google.com/bigquery/docs/querying-wildcard-tables#scanning_a_range_of_tables_using_table_suffix"><code>TABLE_SUFFIX</code></a> to query from 1st Mar to 3rd Mar.</p>
<p>With this, we successfully developed an API over BigQuery that can track user uninstalls on the given date range.</p>
<h3 id="heading-periodic-notifications-via-airflow">Periodic notifications via Airflow</h3>
<p>Once we developed an API, we wanted to utilize it the most by pushing automatic reminders to our users to have the app installed. At InCred, we use <a target="_blank" href="https://airflow.apache.org/">Apache Airflow</a> to program, schedule, and monitor our workflows. Airflow provides a very neat dashboard to monitor your jobs, logging, retry policy, and much more via <a target="_blank" href="https://en.wikipedia.org/wiki/Directed_acyclic_graph">DAG</a> runs.</p>
<p><strong>Let’s break down the problem</strong>. We would get the users with active loans(SQL query to our database), run those users on our Uninstall tracking API, and then send notifications via SMS to all those who have uninstalled our Android app.</p>
<p>Since the users with active loans would be huge, running every logic on a single DAG would take a lot of time to complete. Also, if there are a huge number of users then our SQL query underneath BigQuery would significantly increase thus hitting the <a target="_blank" href="https://cloud.google.com/bigquery/quotas">Maximum unresolved standard SQL query length of 1 MB</a>. So, we broke this problem even further and created multiple child DAGs.</p>
<ul>
<li>We set our master DAG to get the users having active loans.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697537556723/6104cffc-c546-4d39-a8a0-7d8001982a7c.png" alt="Master DAG" /></p>
<ul>
<li>We configured the batch size on Airflow to create a child DAG for every batch.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697537558910/30b731c8-8160-4d1c-a5f9-dbc21dc0e3e9.png" alt="Child DAG" /></p>
<ul>
<li><p>These child DAGs would run in parallel to get install status and send notifications.</p>
</li>
<li><p>Child DAGs would have their retry policy along with master DAG.</p>
</li>
</ul>
<p>Once we brainstormed on the solution, with Airflow, creating this setup was a piece of cake. We did just that and executed our workflow to work like a charm. We then went ahead and scheduled it bi-weekly for periodic reminders to our users.</p>
]]></content:encoded></item><item><title><![CDATA[Kotlin Everywhere. Coroutines, Tests, Robots and much more…]]></title><description><![CDATA[The year is 2019 and with every passing month, technology and the stack underneath are upgrading. For eg: The Android permissions which were open for all, became dangerous, then restricted, and then permissible only when required. Things are changing...]]></description><link>https://blogs.reactivedroid.com/kotlin-everywhere-coroutines-tests-robots-and-much-more-b02030206cc9</link><guid isPermaLink="true">https://blogs.reactivedroid.com/kotlin-everywhere-coroutines-tests-robots-and-much-more-b02030206cc9</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[Android]]></category><dc:creator><![CDATA[Ashwini Kumar]]></dc:creator><pubDate>Fri, 02 Aug 2019 08:01:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/azJJSiwWW90/upload/1bff3965ff9a2b675a460a55f40f37f4.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The year is 2019 and with every passing month, technology and the stack underneath are upgrading. For eg: The Android permissions which were open for all, became dangerous, then restricted, and then permissible only when required. Things are changing fast for the better and more solid Android development with core principles getting intact.</p>
<blockquote>
<p>Change is the only constant — Mahatma Gandhi</p>
</blockquote>
<p>Two years back when <a target="_blank" href="https://github.com/reactivedroid/TvFlix">TvFlix repository</a> was created, the aim was to follow the latest tech stack and architecture. So the app was first designed in MVP pattern and then last year <a target="_blank" href="https://medium.com/mindorks/migration-from-mvp-to-mvvm-using-android-architecture-components-4bc058a1f73c">migrated to MVVM by Android Architecture Components powered by RxJava and Dagger 2</a>. Now, the complete TvMaze repository has been rewritten in Kotlin with the latest tech stack and language goodness. So let’s dive in.</p>
<h3 id="heading-upgrade-to-moshi">Upgrade to Moshi</h3>
<p>TvMaze earlier relied upon <a target="_blank" href="https://github.com/google/gson">Gson</a> for converting JSON to POJO and back. To promote immutability, <a target="_blank" href="https://github.com/google/auto/">AutoValue</a> was being used with <a target="_blank" href="https://github.com/rharter/auto-value-parcel">Parcelable</a> and <a target="_blank" href="https://github.com/rharter/auto-value-gson">Gson</a> extensions by <a target="_blank" href="https://medium.com/u/eeff4b2fb500">Ryan Harter</a>. Gson TypeAdapters helped in avoiding reflection while making the conversion. AutoValue also helped in creating builders while still maintaining immutability. A typical AutoValue class will look like this:</p>
<p>Too much of a boilerplate isn’t it?! Also, since the libraries are moving to Kotlin first then why not upgrade for a better future with the goodness of the language? <a target="_blank" href="https://github.com/square/moshi">Moshi</a> solves all of the above to stand out as a better JSON serialization library. It provides first-hand Kotlin support with <code>moshi-kotlin</code> and <a target="_blank" href="https://medium.com/@ZacSweers/exploring-moshis-kotlin-code-gen-dec09d72de5e"><code>codegen</code></a>. Kotlin data classes are immutable and <code>copy</code> the method helps in creating the builder. Kotlin's <code>@Parcelize</code> helps to replace the AutoValue parcel extension thus removing the complete AutoValue library. So the above data class becomes pretty neat and small.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Parcelize</span>
<span class="hljs-meta">@JsonClass(generateAdapter = true)</span>
<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Episode</span></span>(
<span class="hljs-keyword">val</span> id: <span class="hljs-built_in">Long</span>,
<span class="hljs-keyword">val</span> url: String?) : Parcelable
</code></pre>
<h3 id="heading-migration-to-coroutine-from-rxjava-a-paradigm-shift">Migration to Coroutine from RxJava — A Paradigm Shift</h3>
<p><a target="_blank" href="https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html">Coroutines</a> are new ways to encourage asynchronous programming. They help in synchronously writing asynchronous operations. There are tons of articles that already explain the Whys and Hows of Coroutines. So let’s jump directly into implementation.</p>
<p>With <a target="_blank" href="https://github.com/square/retrofit">Retrofit</a> latest release, Coroutine is officially supported. That means you can remove retrofit’s <code>RxJava2CallAdapterFactory</code> . Just make all the retrofit calls to <code>suspend</code> functions and return the values directly.</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">TvMazeApi</span> </span>{
    <span class="hljs-meta">@GET(<span class="hljs-meta-string">"/schedule"</span>)</span>
    <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getCurrentSchedule</span><span class="hljs-params">(
        <span class="hljs-meta">@Query(<span class="hljs-meta-string">"country"</span>)</span> country: <span class="hljs-type">String</span>,
        <span class="hljs-meta">@Query(<span class="hljs-meta-string">"date"</span>)</span> date: <span class="hljs-type">String</span>
    )</span></span>: List&lt;Episode&gt;

    <span class="hljs-meta">@GET(<span class="hljs-meta-string">"/shows"</span>)</span>
    <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getShows</span><span class="hljs-params">(<span class="hljs-meta">@Query(<span class="hljs-meta-string">"page"</span>)</span> pageNumber: <span class="hljs-type">Int</span>)</span></span>: List&lt;Show&gt;
}
</code></pre>
<p>The home screen of TvMaze shows the list of shows fetched from the TVDB API clubbing them with the user’s favorites. A user can mark any show as a favorite which gets stored via <a target="_blank" href="https://developer.android.com/topic/libraries/architecture/room">Room</a>. Room library now comes with Coroutine support which means all the Dao operations can be suspended thus removing RxJava completely.</p>
<p>Now we are ready to load data for Home screen. <code>HomeViewModel</code> previously relied on RxJava heavily. When the <code>onScreenCreated</code> was called, two streams: one from API and the other from DB were zipped using <code>Single.zip</code>to give the combined result.</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onScreenCreated</span><span class="hljs-params">()</span> </span>{
    isLoading.setValue(<span class="hljs-keyword">true</span>);  
    String currentDate = getCurrentDate();   

    Single&lt;List&lt;Show&gt;&gt; favoriteShows = favoriteShowsRepository
    .getAllFavoriteShows()
    .map(<span class="hljs-keyword">this</span>::convertToShows);       

    Single&lt;List&lt;Episode&gt;&gt; episodes = tvMazeApi.getCurrentSchedule(COUNTRY_US, currentDate);
    Single&lt;List&lt;Episode&gt;&gt; zippedSingleSource = 
    Single.zip(episodes, favoriteShows, <span class="hljs-keyword">this</span>::favorites);
Disposable homeDisposable = zippedSingleSource.observeOn(AndroidSchedulers.mainThread())
    .subscribeOn(Schedulers.io())
    .subscribe(<span class="hljs-keyword">this</span>::onSuccess, <span class="hljs-keyword">this</span>::onError);
    compositeDisposable.add(homeDisposable);   
}
</code></pre>
<p>Let's convert the above function to coroutines. Since Retrofit and Room are already on Coroutine support, they respect the <code>suspend</code> calls. That means, <code>viewModelScope</code> will launch a new coroutine, which then calls retrofit’s suspend function <code>tvMazeApi.getCurrentSchedule</code>. The retrofit will move the network operation to the IO-bound coroutine via <code>Dispatchers.IO</code> and once the suspending function is complete, it will return the result on <code>Dispatchers.Main</code>. A similar case will happen with Room <code>suspend</code> calls.</p>
<pre><code class="lang-kotlin"><span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onScreenCreated</span><span class="hljs-params">()</span></span> {
    homeViewStateLiveData.value = Loading
    <span class="hljs-keyword">val</span> coroutineExceptionHandler = CoroutineExceptionHandler { _, exception -&gt;
        onError(exception)
    }
    <span class="hljs-comment">// viewModelScope launches the new coroutine on Main Dispatcher internally</span>
    viewModelScope.launch(coroutineExceptionHandler) {
        <span class="hljs-comment">// Get favorite shows from db, suspend function in room will launch a new coroutine with IO dispatcher</span>
        <span class="hljs-keyword">val</span> favoriteShowIds = favoriteShowsRepository.allFavoriteShowIds()
        <span class="hljs-comment">// Get shows from network, suspend function in retrofit will launch a new coroutine with IO dispatcher</span>
        <span class="hljs-keyword">val</span> episodes = tvMazeApi.getCurrentSchedule(COUNTRY_US, currentDate)

        <span class="hljs-comment">// Return the result on main thread via Dispatchers.Main</span>
        homeViewStateLiveData.value = Success(HomeViewData(getShowsWithFavorites(episodes, favoriteShowIds)))
    }
}
</code></pre>
<p><code>viewModelScope</code> is a lifecycle Kotlin extension to support coroutines. This handles the cancellation of the coroutine when ViewModel <code>onCleared</code> is called so that you don’t have to worry about holding the <code>Job</code> instance and explicitly canceling it. <code>CoroutineExceptionHandler</code> handles the exceptions being thrown at any Coroutine so that the errors can be propagated and displayed to the user.</p>
<h3 id="heading-unit-testing-coroutine">Unit Testing Coroutine</h3>
<p>We have introduced Coroutine to replace RxJava both on the network and DB layer. Time to put them to the test. We will use <a target="_blank" href="https://github.com/mockito/mockito">Mockito</a> with <code>mockito-kotlin</code> for better mocking APIs. The test will run on <code>RobolectricTestRunner</code> to incorporate Android resources into testing.</p>
<h4 id="heading-testing-room-with-coroutine">Testing Room With Coroutine</h4>
<p>TvFlix stores the user-marked favorite shows in Db. All the CRUD operations happen via <code>FavoriteShowsRepository</code></p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="0f22f2e0392aed60640c6151ebff1450"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/reactivedroid/0f22f2e0392aed60640c6151ebff1450" class="embed-card">https://gist.github.com/reactivedroid/0f22f2e0392aed60640c6151ebff1450</a></div><p> </p>
<p>To test <code>FavoriteShowsRepository</code>, we will create an in-memory database via <code>Room.inMemoryDatabaseBuilder</code>. The in-memory database provides faster CRUD operations on the database and also ensures that the database disappears when the process is killed. To effectively test coroutines, we will rely on <a target="_blank" href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/">Coroutine test library</a>(still in development). Let’s check the <code>FavoriteShowsRepositoryTest</code> class and understand the flow:</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="9451a743bdfd767b6159600811130408"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/reactivedroid/9451a743bdfd767b6159600811130408" class="embed-card">https://gist.github.com/reactivedroid/9451a743bdfd767b6159600811130408</a></div><p> </p>
<p>We mock the context and provide it to Room to create the database. Mocking of context is possible by <code>mockito-inline</code> which helps in mocking final classes. With the database, we get the mock <code>showDao</code> and create our repository. <code>runBlocking</code> runs the new coroutine on the current thread until its completion. We launch the new Coroutine or run the suspending function in this block. We have used <a target="_blank" href="https://github.com/google/truth"><code>Truth</code></a> library which provides better and more expressive assertions.</p>
<h4 id="heading-testing-viewmodel">Testing ViewModel</h4>
<p><code>InstantTaskExecutorRule</code> is an Android architecture testing component that executes the task in the same thread. <code>MainCoroutineRule</code> sets the main Coroutines Dispatcher to <code>TestCoroutineScope</code> for better control over coroutine execution in unit testing. <code>HomeViewModel</code> is dependent upon <code>TvMazeApi</code> and <code>FavoriteShowsRepository</code>. So, we mock the required dependencies and then use <code>@InjectMocks</code> to mock <code>HomeViewModel</code>. To test, if the home screen is loaded with shows and without favorites, we first stub network and DB calls. Then, we verify whether the loader is shown or not. <code>pauseDispatcher</code> pauses the execution of a coroutine to verify the initial <code>Loading</code> state. After verification, we <code>resumeDispatcher</code> to proceed with execution of pending coroutine actions.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="a5d4317486193a8a7e4eac74d3aece20"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/reactivedroid/a5d4317486193a8a7e4eac74d3aece20" class="embed-card">https://gist.github.com/reactivedroid/a5d4317486193a8a7e4eac74d3aece20</a></div><p> </p>
<p>Initially, when <code>HomeViewModelTest</code> was run, it failed because of the way the tests were written. Read more <a target="_blank" href="https://stackoverflow.com/questions/57239510/coroutine-unit-testing-fails-for-the-class">here</a>. Then after further readings and listening to an awesome <a target="_blank" href="http://androidbackstage.blogspot.com/2019/07/episode-117-kotlin-coroutines.html">Podcast</a>, it was found that we don’t explicitly have to worry about context-switching with Retrofit and Room. The libraries that come with Coroutine support, own the <code>suspend</code> functions and move them off the UI thread on their own. Thus, they reduce the hassles of handling thread callbacks by the developers in a big way. The result is pretty neat asynchronous calls written synchronously with no callbacks and better readability.</p>
<h3 id="heading-ui-testing-robot-pattern">UI Testing — Robot Pattern</h3>
<p><a target="_blank" href="https://academy.realm.io/posts/kau-jake-wharton-testing-robots/">Robot Pattern</a> was introduced by <a target="_blank" href="https://medium.com/u/8ddd94878165">Jake Wharton</a> to provide a better separation of concern for UI testing. The pattern aims at properly managing the UI test by separating the What and How of it. Meaning that what is to be tested should not concern <em>How</em> it is to be tested. Both should have clear separation so that maintenance of UI tests becomes easy.</p>
<p>Say, we want to test the Home Screen, whether it shows the list of <em>Popular Shows,</em> and whether <em>Add to Favorites</em> is working fine or not. This is the <em>What</em> of Robot Pattern. The <em>How</em> of this will be taken care of by <code>HomeRobot</code> which handles all the verification/assertions/actions via <code>Espresso</code>.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="ed947609a06ee6e550dfffce0decc560"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/reactivedroid/ed947609a06ee6e550dfffce0decc560" class="embed-card">https://gist.github.com/reactivedroid/ed947609a06ee6e550dfffce0decc560</a></div><p> </p>
<p><code>LoadingIdlingResource</code> tells Espresso to wait until the list of<code>shows</code> is fetched and displayed on the screen. This is achieved via the visibility of the progress bar. As an alternative, some suggest using <a target="_blank" href="https://medium.com/androiddevelopers/android-testing-with-espressos-idling-resources-and-testing-fidelity-8b8647ed57f4">Espresso in production code</a>, but having a test code in production does not look good(Debatable). The final piece is our Kotlin Robot <code>HomeRobot</code></p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="f5aa71341c982213b02798f33f5e13d8"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/reactivedroid/f5aa71341c982213b02798f33f5e13d8" class="embed-card">https://gist.github.com/reactivedroid/f5aa71341c982213b02798f33f5e13d8</a></div><p> </p>
<p>This Robot is loaded with Kotlin goodness to remove the builder pattern and use language advanced primitives. Know more <a target="_blank" href="https://academy.realm.io/posts/kau-jake-wharton-testing-robots/">here</a>.</p>
<h3 id="heading-future-is-bright">Future is Bright</h3>
<p>RxJava is a powerful library for asynchronous programming packed with tons of operators to get along. Coroutines are still new, and the Android community is slowly accepting them. Heavy development around <a target="_blank" href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/">Flow</a> and <a target="_blank" href="https://kotlinlang.org/docs/reference/coroutines/channels.html">Channels</a>, are bridging this gap. Kotlin surely bags a punch with superb language features. With an awesome community, the road to the future of Android Application development looks even brighter. So why wait, go ahead and give a boost to your Android development with Kotlin now. 🚀</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/reactivedroid/TvFlix">https://github.com/reactivedroid/TvFlix</a></div>
]]></content:encoded></item></channel></rss>