<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Angel G. Olloqui</title>
    <description>The latest articles on DEV Community by Angel G. Olloqui (@angelolloqui).</description>
    <link>https://dev.to/angelolloqui</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F89672%2F9f31089c-3a98-456c-a897-d6eecbc8a424.jpeg</url>
      <title>DEV Community: Angel G. Olloqui</title>
      <link>https://dev.to/angelolloqui</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/angelolloqui"/>
    <language>en</language>
    <item>
      <title>AI &amp; Automation 2025 Yearly Wrap-Up</title>
      <dc:creator>Angel G. Olloqui</dc:creator>
      <pubDate>Fri, 23 Jan 2026 11:06:50 +0000</pubDate>
      <link>https://dev.to/playtomic/ai-automation-2025-yearly-wrap-up-4ckl</link>
      <guid>https://dev.to/playtomic/ai-automation-2025-yearly-wrap-up-4ckl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This post is an adaptation of an internal wrap-up I shared at Playtomic to celebrate our AI journey in 2025. I've kept the public-facing industry insights and general learnings while removing internal-only details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Happy 2026! 🎉 As we kick off the new year, let's take a moment to celebrate 2025—the year AI went from "cool experiment" to "wait, this is actually can change everything." This wrap-up captures the industry's most transformative developments alongside a look at how AI and automation are becoming integral to modern product development and operations. 🎊&lt;/p&gt;




&lt;h2&gt;
  
  
  🌍 AI IN THE INDUSTRY
&lt;/h2&gt;

&lt;p&gt;💡 &lt;em&gt;What happened in AI this year? &lt;a href="https://docs.google.com/presentation/d/1xiLl0VdrlNMAei8pmaX4ojIOfej6lhvZbOIK7Z6C-Go/edit?slide=id.g309a25a756d_0_85#slide=id.g309a25a756d_0_85" rel="noopener noreferrer"&gt;A lot.&lt;/a&gt; Here's what you need to know:&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 The Tech Race Heats Up
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;OpenAI still leads, but the competition is fierce.&lt;/strong&gt; China's open-source models (DeepSeek, Qwen, Kimi and GLM) are gaining ground fast, while Google's Gemini 3 Pro &lt;a href="https://llm-stats.com/" rel="noopener noreferrer"&gt;dominates reasoning benchmarks&lt;/a&gt; and Claude remains the go-to for software development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reasoning models are evolving rapidly.&lt;/strong&gt; These "think before you answer" AIs can now plan, reflect, self-correct, and work over longer time horizons—making them smarter problem-solvers. But humans still outperform AI on &lt;a href="https://galileo.ai/blog/humanitys-last-exam-ai-benchmark" rel="noopener noreferrer"&gt;Humanity's Last Exam&lt;/a&gt;, a benchmark designed to test the very edge of human knowledge. For now, we're still the champions. 💪&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multimodal, Image and Video AI just leveled up.&lt;/strong&gt; &lt;a href="https://aistudio.google.com/models/veo-3" rel="noopener noreferrer"&gt;Google's Veo 3&lt;/a&gt; and &lt;a href="https://openai.com/index/sora-2/" rel="noopener noreferrer"&gt;OpenAI's Sora 2&lt;/a&gt; now generate high-quality 8-second videos with audio and sound effects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First World models on stage.&lt;/strong&gt; Google also dropped &lt;a href="https://deepmind.google/blog/genie-3-a-new-frontier-for-world-models/" rel="noopener noreferrer"&gt;Genie 3&lt;/a&gt; and Runway &lt;a href="https://runwayml.com/research/introducing-runway-gwm-1?utm_source=www.therundown.ai&amp;amp;utm_medium=newsletter&amp;amp;utm_campaign=disney-s-billion-dollar-ai-bet-on-openai&amp;amp;_bhlid=7146b55d482c5b838bccfed809a074c48378e477#worlds-section" rel="noopener noreferrer"&gt;GWM-1&lt;/a&gt;, interactive world models that creates virtual environments you can explore and interact with in real time. Expect lots of innovation around this on 2026 🎬&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP (Model Context Protocol), aka "USB-C for AI," is officially everywhere&lt;/strong&gt;—companies are racing to support it, even if &lt;a href="https://newsletter.pragmaticengineer.com/p/mcp-deepdive" rel="noopener noreferrer"&gt;users are still figuring out where to plug it in&lt;/a&gt;. 🔌 Think of it as the universal connector AI has been waiting for.&lt;/p&gt;

&lt;h3&gt;
  
  
  💰 Industry &amp;amp; Infrastructure
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Mega investments are pouring in.&lt;/strong&gt; Case in point: &lt;em&gt;&lt;a href="https://en.wikipedia.org/wiki/Stargate_LLC" rel="noopener noreferrer"&gt;Stargate&lt;/a&gt;&lt;/em&gt;—a $500B, 10GW US mega-cluster backed by Altman, SoftBank, Ellison, and Trump. 4 million GPUs working together! 🤯&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chips are still the bottleneck.&lt;/strong&gt; Nvidia produces ~90% of AI's horsepower while US controls 75% of global AI capacity—though China is racing to close the gap despite chip export restrictions. 🔬&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Electricity is now the new limiting factor.&lt;/strong&gt; China added 400GW of power infrastructure in 2024 vs 40GW in the US. Meanwhile, some companies are exploring space-based solar power plants— the sky isn't the limit anymore. 🛰️&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From courtroom to conference room.&lt;/strong&gt; After a wave of lawsuits from Disney, BBC, Getty Images, The New York Times,… over unauthorized training data, 2025 brought détente: licensing deals. Now, content partnerships are the new normal—and a &lt;a href="https://thewaltdisneycompany.com/disney-openai-sora-agreement/?utm_source=www.therundown.ai&amp;amp;utm_medium=newsletter&amp;amp;utm_campaign=disney-s-billion-dollar-ai-bet-on-openai&amp;amp;_bhlid=10d122b4c29835ea12a299cc8644453cb4c1dcc3" rel="noopener noreferrer"&gt;fresh revenue&lt;/a&gt; stream for publishers. 🤝&lt;/p&gt;

&lt;h3&gt;
  
  
  🏢 Enterprise Adoption
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;2025 was the year of AI agents&lt;/strong&gt;—mostly text-based, but voice is ramping up. Still, most companies are stuck in &lt;a href="https://www.mckinsey.com/capabilities/quantumblack/our-insights/the-state-of-ai" rel="noopener noreferrer"&gt;pilot phase&lt;/a&gt;. Meanwhile at Playtomic? We said "hold my racket" 🎾 and shipped agents straight to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Executives are optimistic, but expertise lags behind.&lt;/strong&gt; &lt;a href="https://www.accenture.com/us-en/insights/consulting/gen-ai-reinventing-enterprise-models" rel="noopener noreferrer"&gt;97% of executives&lt;/a&gt; believe AI will transform their industry and company, yet only 35% say they have the expertise needed to drive those transformations. It seems everyone wants to ride the AI wave, but most are still looking for their surfboard. 🏄‍♂️&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Supernova AI startups"&lt;/strong&gt; are a new category: AI companies growing faster than anything we've seen before. 🚀&lt;/p&gt;

&lt;h3&gt;
  
  
  🌐 Politics &amp;amp; Jobs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;AI politics hardened.&lt;/strong&gt; The US doubled down on "America-first AI," Europe's AI Act stumbled, and China expanded its open-source ecosystem and domestic chip ambitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Defense goes all-in on AI.&lt;/strong&gt; The US Army dropped a &lt;a href="https://www.palantir.com/offerings/defense/army/" rel="noopener noreferrer"&gt;$10B deal with Palantir&lt;/a&gt;, NATO adopted &lt;a href="https://shape.nato.int/news-releases/nato-acquires-aienabled-warfighting-system-" rel="noopener noreferrer"&gt;Maven&lt;/a&gt; in record time, and the Pentagon brought in &lt;a href="https://www.cnbc.com/2025/07/14/anthropic-google-openai-xai-granted-up-to-200-million-from-dod.html" rel="noopener noreferrer"&gt;top companies&lt;/a&gt; for AI experiments. From "maybe we shouldn't" to "here's $200M"—quite the vibe shift🛡️&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Job shifts happening slowly.&lt;/strong&gt; AI is &lt;a href="https://www.reuters.com/technology/online-marketplace-fiverr-lay-off-30-workforce-ai-push-2025-09-15/" rel="noopener noreferrer"&gt;beginning to affect&lt;/a&gt; some roles (with &lt;a href="https://www.goldmansachs.com/insights/articles/how-will-ai-affect-the-global-workforce" rel="noopener noreferrer"&gt;up to 7% future displacement predicted&lt;/a&gt; by Goldman Sachs), but impact &lt;a href="https://econofact.org/factbrief/fact-check-has-ai-already-caused-some-job-displacement" rel="noopener noreferrer"&gt;remains modest&lt;/a&gt;. Meanwhile, new jobs are emerging: AI ethicists, ML engineers, data specialists…&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚖️ Safety &amp;amp; Ethics
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Make it safe.&lt;/strong&gt; There's strong public interest in alignment (making sure AI does what we want), reliability, and explainability—but budgets remain relatively low.👿&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From "vibe coding" to "vibe hacking".&lt;/strong&gt; AI is already being used for &lt;a href="https://www.anthropic.com/news/disrupting-AI-espionage?_bhlid=e7c247a8feb826c38f8372f10146eafa395ab38a" rel="noopener noreferrer"&gt;cyberattacks&lt;/a&gt;, exploiting code faster than humans can fix it. In parallel, several &lt;a href="https://github.com/aliasrobotics/cai" rel="noopener noreferrer"&gt;projects&lt;/a&gt; and &lt;a href="https://openai.com/index/introducing-aardvark/" rel="noopener noreferrer"&gt;companies&lt;/a&gt; are also exploring AI as security researchers 🔓.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deepfakes and AI-generated video&lt;/strong&gt; are raising new ethical and legal questions, while researches show models can now "&lt;a href="https://www.anthropic.com/research/alignment-faking" rel="noopener noreferrer"&gt;fake alignment&lt;/a&gt;" when being watched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Model Welfare debates are starting to emerge.&lt;/strong&gt; Researchers and ethicists are now discussing whether AI should merit moral considerations—raising questions about consciousness, sentience, and the ethical treatment. Still early, but more to be expected as systems become increasingly sophisticated. 🤔&lt;/p&gt;

&lt;h3&gt;
  
  
  🔬 AI Beyond Tech
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Chemistry:&lt;/strong&gt; &lt;a href="https://deepmind.google/science/alphafold/" rel="noopener noreferrer"&gt;AlphaFold&lt;/a&gt; and &lt;a href="https://www.profluent.bio/showcase/progen3" rel="noopener noreferrer"&gt;ProGen3&lt;/a&gt; are predicting 3D protein structures and accelerating drug and material discovery.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Healthcare:&lt;/strong&gt; AI is revolutionizing &lt;a href="https://www.aidoc.com/eu/" rel="noopener noreferrer"&gt;diagnostics&lt;/a&gt; and enabling &lt;a href="https://www.tempus.com/" rel="noopener noreferrer"&gt;personalized cancer treatments&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Robotics:&lt;/strong&gt; Major hype around humanoid robots like &lt;a href="https://www.figure.ai/" rel="noopener noreferrer"&gt;Figure 3&lt;/a&gt; and &lt;a href="https://www.tesla.com/en_eu/AI" rel="noopener noreferrer"&gt;Tesla's Optimus&lt;/a&gt; but still far from deployment. Meanwhile, autonomous driving is ramping up fast, with &lt;a href="https://waymo.com/" rel="noopener noreferrer"&gt;Waymo&lt;/a&gt; leading with more than &lt;a href="https://waymo.com/blog/2025/12/2025-year-in-review" rel="noopener noreferrer"&gt;14M rides in 2025&lt;/a&gt; without human drivers.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Other cool stuff:&lt;/strong&gt; AI is now predicting &lt;a href="https://blog.google/technology/google-deepmind/weathernext-2/?utm_source=ai.google&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;weather&lt;/a&gt;, spotting &lt;a href="https://research.google/blog/teaching-gemini-to-spot-exploding-stars-with-just-a-few-examples/?utm_source=ai.google&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;exploding stars&lt;/a&gt;, detecting &lt;a href="https://blog.google/technology/research/first-firesat-images/?utm_source=ai.google&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;wildfires&lt;/a&gt;, and even trying to &lt;a href="https://deepmind.google/models/gemma/dolphingemma/" rel="noopener noreferrer"&gt;understand dolphin language&lt;/a&gt;. 🐬&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎾 AI AT PLAYTOMIC
&lt;/h2&gt;

&lt;p&gt;💡 &lt;em&gt;How we've been applying these trends:&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🏁 Product Innovation
&lt;/h3&gt;

&lt;p&gt;We've integrated AI into several key areas of our product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;AI-Driven Customer Care:&lt;/strong&gt; We've deployed super agents that instantly answer questions from clubs and players, significantly improving response times and customer satisfaction.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Personalized Insights:&lt;/strong&gt; Using AI, we've built dashboards for clubs that provide tailored analytics and reports, helping them make better data-driven decisions.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Virtual Buddies:&lt;/strong&gt; AI-generated avatars now help in our educational content, speaking multiple languages and making our product more accessible globally.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Conversational AI:&lt;/strong&gt; We experimented with AI-driven Whatsapp chatbots to explore new ways of user interaction and distribution strategies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🧑‍💻 Engineering &amp;amp; Operations
&lt;/h3&gt;

&lt;p&gt;AI has transformed our internal workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;AI-Powered Development:&lt;/strong&gt; Tools like Cursor and Claude Code have changed how we build, helping us write code faster, plan better architectures, and explore complex codebases.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Operational Automation:&lt;/strong&gt; We've automated repetitive tasks in data analytics, QA processes, and customer support triage, saving hundreds of hours of manual work every month.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Fraud Detection:&lt;/strong&gt; Our AI-powered systems are actively identifying and blocking fraudulent transactions, protecting our ecosystem.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🤔 DID YOU KNOW?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Universal Search:&lt;/strong&gt; Tools like &lt;a href="https://www.notion.com/product/enterprise-search" rel="noopener noreferrer"&gt;Notion AI&lt;/a&gt; now pull information from multiple sources (Slack, GitHub, email), creating a single entry point for knowledge.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Plan Mode:&lt;/strong&gt; The shift towards "spec-first" development—where AI drafts a plan before execution—is leading to more thoughtful architecture and fewer mistakes.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Breaking Silos:&lt;/strong&gt; AI is enabling unprecedented collaboration between engineers and non-engineers, allowing PMs and designers to contribute more directly to the technical process.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📚 WANT TO LEARN MORE?
&lt;/h2&gt;

&lt;p&gt;Here are some fantastic resources to level up your AI game:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;📖 Reading: AI Engineering&lt;/strong&gt; – &lt;a href="https://www.amazon.es/AI-Engineering-Building-Applications-Foundation/dp/1098166302" rel="noopener noreferrer"&gt;Grab this book&lt;/a&gt; if you're ready to go deep into foundation models and AI applications.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;🎬 Watching: The Thinking Game&lt;/strong&gt; – &lt;a href="https://www.youtube.com/watch?v=d95J8yzvjbQ" rel="noopener noreferrer"&gt;This documentary&lt;/a&gt; follows DeepMind's journey from AlphaGo to AlphaFold. It's a must-watch!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Buckle up—2026 is coming in hot! 🚀&lt;/strong&gt; We're not slowing down. The future isn't just arriving—we're building it, one serve at a time. Game on! 🎾⚡&lt;/p&gt;

</description>
      <category>ai</category>
      <category>engineering</category>
      <category>automation</category>
    </item>
    <item>
      <title>Adding multi device previews in Xcode and Android Studio</title>
      <dc:creator>Angel G. Olloqui</dc:creator>
      <pubDate>Wed, 05 Apr 2023 09:48:47 +0000</pubDate>
      <link>https://dev.to/playtomic/adding-multi-device-previews-in-xcode-and-android-studio-4cc4</link>
      <guid>https://dev.to/playtomic/adding-multi-device-previews-in-xcode-and-android-studio-4cc4</guid>
      <description>&lt;p&gt;Developing with SwiftUI and Jetpack Compose is very user-friendly. Both frameworks offer a highly readable declarative syntax and a live preview feature that updates in real-time while coding. This live preview allows developers to easily configure various device settings, although it can be a bit cumbersome to specify multiple device factors. At Playtomic, we have created a simple utility for both platforms that allows us to run our previews on a predefined set of devices. Here's what we did:&lt;/p&gt;

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

&lt;p&gt;This platform was the easy one, as it already supports aggregated annotations out of the box. All you need to do is include somewhere in your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Preview(showSystemUi = true, device = Devices.PIXEL_4_XL, name = "Pixel 4 XL")   // big xxxhdpi
@Preview(showSystemUi = true, device = Devices.NEXUS_5, name = "Nexus 5")   // small-medium xxhdpi
annotation class MultiDevicePreview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then just replace the usage of &lt;code&gt;@Preview&lt;/code&gt; by &lt;code&gt;@MultiDevicePreview&lt;/code&gt; in your previews:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@MultiDevicePreview
@Composable
private fun AMultiDevicePreview() {
    Text("Multidevice preview")
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0kCh8AeF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/73578b7e3mdi5qeyosji.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0kCh8AeF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/73578b7e3mdi5qeyosji.png" alt="Screenshot from preview in Android Studio using multiple device previews" width="603" height="648"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  iOS
&lt;/h2&gt;

&lt;p&gt;Things become a bit more complicated for iOS. Firstly, Xcode only recognizes previews that implement the &lt;code&gt;PreviewProvider&lt;/code&gt; protocol. As a result, extending the interface with our custom version of &lt;code&gt;MultiDevicePreviewProvider&lt;/code&gt; doesn't work. Instead, we created a second protocol to tackle this problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public protocol MultiDevicePreview {
    associatedtype DevicePreviews : View
    @ViewBuilder @MainActor static var devicePreviews: DevicePreviews { get }

    @MainActor static var devices: [PreviewDevice] { get }
    @MainActor static var previewName: String? { get }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then, we provided a default implementation of the previews for those using the new protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public extension PreviewProvider where Self: MultiDevicePreview {
    static var previews: some View {
        ForEach(devices) { device in
            AnyView(devicePreviews)
                .previewDevice(device)
                .previewDisplayName([previewName, device.rawValue].compactMap { $0 }.joined(separator: " - "))
        }
    }

    static var devices: [PreviewDevice] { PreviewDevice.allCases }
    static var previewName: String? { String(describing: Self.self) }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also declared some predefined list of devices to simplify the management:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;extension PreviewDevice {
    public static let iPhone14 = PreviewDevice("iPhone 14")
    public static let iPhone14Max = PreviewDevice("iPhone 14 Pro Max")
    public static let iPhoneSE = PreviewDevice("iPhone SE (3rd generation)")
    public static let allCases = [iPhone14, iPhone14Max, iPhoneSE]
}

extension PreviewDevice: Identifiable {
    public var id: String {
        rawValue
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With all of the above, you can make use of multidevice previews by conforming your preview to &lt;code&gt;MultiDevicePreview&lt;/code&gt; and replace the method &lt;code&gt;previews&lt;/code&gt; by &lt;code&gt;devicePreviews&lt;/code&gt; like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;struct AMultiDevicePreview: PreviewProvider, MultiDevicePreview {
    static var devicePreviews: some View {
        Text("multidevice preview text")
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VB0BicC0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kpzlxakk41vj7881fln6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VB0BicC0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kpzlxakk41vj7881fln6.png" alt="Scheenshot from Xcode using multiple device previews" width="880" height="778"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not a huge difference, but a nice small addition to the toolkit!&lt;/p&gt;

</description>
      <category>ios</category>
      <category>android</category>
      <category>swiftui</category>
      <category>compose</category>
    </item>
    <item>
      <title>Data Races with value types in Swift</title>
      <dc:creator>Angel G. Olloqui</dc:creator>
      <pubDate>Fri, 26 Aug 2022 14:49:54 +0000</pubDate>
      <link>https://dev.to/playtomic/data-races-with-value-types-in-swift-5e45</link>
      <guid>https://dev.to/playtomic/data-races-with-value-types-in-swift-5e45</guid>
      <description>&lt;p&gt;This week I had an &lt;a href="https://github.com/GetStream/swift-activity-feed/issues/19"&gt;interesting discussion around a possible data race condition&lt;/a&gt; due to wrong threading synchronisation when manipulating a value type (a &lt;code&gt;String&lt;/code&gt; in this case) in a class. &lt;/p&gt;

&lt;h2&gt;
  
  
  The buggy code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;final class MyClass {
    var token: String

    init(_ token: String = "") {
        self.token = token
    }

    func myMethod() -&amp;gt; Bool {
        token.isEmpty
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a first look, this might seem correct. We have just a &lt;code&gt;var&lt;/code&gt; with a &lt;code&gt;String&lt;/code&gt;, which is a value type, and a method to just check if the &lt;code&gt;token&lt;/code&gt; is empty that only calls the &lt;code&gt;isEmpty&lt;/code&gt; from &lt;code&gt;String&lt;/code&gt;. Straightforward code and safe right? well, it is OK as long as you do not introduce threading, but the moment you do it will not. Let me elaborate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;If you run this test with Thread Sanitizer enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; func test_data_race() {
        let sut = MyClass()

        DispatchQueue.concurrentPerform(iterations: 1_000_000) { i in
            sut.token = "\(i)"
            _ = sut.myMethod()
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you will see this output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WARNING: ThreadSanitizer: data race (pid=8329)
  Read of size 8 at 0x000107c1aab8 by thread T2:
    #0 closure #1 in DataTests.test_data_race() DataTests.swift:69 (Tests:arm64+0xde354)
    #1 partial apply for closure #1 in DataTests.test_data_race() &amp;lt;compiler-generated&amp;gt; (Tests:arm64+0xde3e4)
    #2 partial apply for thunk for @callee_guaranteed (@unowned Int) -&amp;gt; () &amp;lt;null&amp;gt;:73675156 (libswiftDispatch.dylib:arm64+0x42f4)
    #3 _dispatch_client_callout2 &amp;lt;null&amp;gt;:73675156 (libdispatch.dylib:arm64+0x35dc)

  Previous write of size 8 at 0x000107c1aab8 by main thread:
    #0 closure #1 in DataTests.test_data_race() DataTests.swift:69 (Tests:arm64+0xde374)
    #1 partial apply for closure #1 in DataTests.test_data_race() &amp;lt;compiler-generated&amp;gt; (Tests:arm64+0xde3e4)
    #2 partial apply for thunk for @callee_guaranteed (@unowned Int) -&amp;gt; () &amp;lt;null&amp;gt;:73675156 (libswiftDispatch.dylib:arm64+0x42f4)
    #3 _dispatch_client_callout2 &amp;lt;null&amp;gt;:73675156 (libdispatch.dylib:arm64+0x35dc)
    #4 _swift_dispatch_apply_current &amp;lt;null&amp;gt;:73675156 (libswiftDispatch.dylib:arm64+0x43a0)
    #5 @objc DataTests.test_data_race() &amp;lt;compiler-generated&amp;gt; (Tests:arm64+0xde448)
    #6 __invoking___ &amp;lt;null&amp;gt;:73675156 (CoreFoundation:arm64+0x11c5ec)

  Location is heap block of size 32 at 0x000107c1aaa0 allocated by main thread:
    #0 __sanitizer_mz_malloc &amp;lt;null&amp;gt;:73675156 (libclang_rt.tsan_iossim_dynamic.dylib:arm64+0x51004)
    #1 _malloc_zone_malloc &amp;lt;null&amp;gt;:73675156 (libsystem_malloc.dylib:arm64+0x1527c)
    #2 DataTests.test_data_race() DataTests.swift:66 (Tests:arm64+0xde07c)
    #3 @objc DataTests.test_data_race() &amp;lt;compiler-generated&amp;gt; (Tests:arm64+0xde448)
    #4 __invoking___ &amp;lt;null&amp;gt;:73675156 (CoreFoundation:arm64+0x11c5ec)

  Thread T2 (tid=6246748, running) is a GCD worker thread

SUMMARY: ThreadSanitizer: data race DataTests.swift:69 in closure #1 in DataTests.test_data_race()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So ThreadSanitizer is detecting a &lt;strong&gt;data race in the code when accessing the token&lt;/strong&gt;. &lt;br&gt;
What does it mean? basically you are making a wrong usage of the variable. It gets read and write operations concurrently but the variable itself is not protected, and the fact that it is a value type does not help.&lt;/p&gt;

&lt;p&gt;What can this cause? it is undefined, but in practice most likely you will have a crash when compilation optimizations are enabled.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;OK, so this simple code can crash when reading and writing the token in parallel from different threads! How can we fix it? we just need to make serial access to read/write. There are multiple ways of doing it (with different primitives), but this could be one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;final class MyClass {
    private let syncQueue = DispatchQueue(label: "com.test.myQueue", attributes: .concurrent)
    private var _token: String
    var token: String {
        get {
            syncQueue.sync {
                _token
            }
        }
        set {
            syncQueue.async(flags: .barrier) {
                _token = newValue
            }
        }
    }

    init(_ token: String = "") {
        _token = token
    }

    func myMethod() -&amp;gt; Bool {
        token.isEmpty
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, what we did is to protect the &lt;code&gt;var&lt;/code&gt; by forcing serial writing to it, so multiple reading can happen but only 1 thread can execute a write at a time (the barrier waits for all previous readings to finish and postpones all subsequent read/write accesses till the write is done). The resulting code is slower to execute, but it is now safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;I wanted to share a few thoughts around this issue, that are common misconceptions in the Swift community:&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Value types are thread safe
&lt;/h3&gt;

&lt;p&gt;Since the value type has a copy semantic, it may seem logical to think that they are inherently protected from data races. However, that is not the case. &lt;strong&gt;Swift does not guarantee thread safety in value types&lt;/strong&gt;, so accessing any &lt;code&gt;var&lt;/code&gt; from multiple threads is a potential data race condition. This issue of course does not apply to &lt;code&gt;let&lt;/code&gt; variables since they are immutable.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Value types are always copied
&lt;/h3&gt;

&lt;p&gt;That is the semantic but not really what happens under the hood. When passing value types around, Swift compiler is smart enough to know if the copy is needed, removing unnecessary copies. In practice it uses a CopyOnWrite(COW) strategy, where it will &lt;strong&gt;make the copy only when the value is modified&lt;/strong&gt;, but not when passed around. As a result, in most situations you will actually have a pointer to the same underlaying memory address even when using value types.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Tests do always behave like production code
&lt;/h3&gt;

&lt;p&gt;The fact that a test does not crash is no guarantee to assert that some code can not crash in production. Tests run in simulated environments and &lt;strong&gt;they normally have different compilation options than the ones in your final builds&lt;/strong&gt;. For example, ARC will make aggressive optimizations when compiling with the proper options, so lots of unnecessary retain/releases will be removed from final builds. In this particular case, my test suit was not crashing, and I was only able to see some wrong usage by activating the Thread Sanitizer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extra reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://forums.swift.org/t/understanding-swifts-value-type-thread-safety/41406"&gt;Understanding Swift's value types thread safety&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://angelolloqui.com/blog/21-ARC-I-Introduction-to-ARC-and-how-it-works-internally"&gt;ARC optimizations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>datarace</category>
      <category>bugs</category>
    </item>
    <item>
      <title>MVI in Playtomic mobile app</title>
      <dc:creator>Angel G. Olloqui</dc:creator>
      <pubDate>Tue, 31 May 2022 12:06:15 +0000</pubDate>
      <link>https://dev.to/playtomic/mvi-at-playtomic-2pea</link>
      <guid>https://dev.to/playtomic/mvi-at-playtomic-2pea</guid>
      <description>&lt;p&gt;Last summer we finally decided to move out of our classic MVP+UIKit/Android view architecture into a more modern one with &lt;strong&gt;SwiftUI&lt;/strong&gt;/&lt;strong&gt;Compose&lt;/strong&gt; as main actors for our view layer. Together with the UI framework changes, we found the need of switching to a more reactive architecture that better fits the nature of declarative UIs.&lt;/p&gt;

&lt;p&gt;We spent some time analysing some of the most popular reactive architectures: MVVM, MVI and &lt;a href="https://github.com/pointfreeco/swift-composable-architecture"&gt;TCA&lt;/a&gt;. Without getting into much detail of our decision making (it would take a full post), we decided that &lt;strong&gt;MVI was the best fitting for our project&lt;/strong&gt;. With it, we could get better &lt;strong&gt;separation of concerns&lt;/strong&gt; and &lt;strong&gt;state management&lt;/strong&gt; than in MVVM, &lt;strong&gt;unidirectional data flow&lt;/strong&gt;, &lt;strong&gt;single source of truth&lt;/strong&gt; and &lt;strong&gt;easy of testing&lt;/strong&gt;, without the additional complexity added by TCA.&lt;/p&gt;

&lt;p&gt;After around half a year working with MVI, the &lt;strong&gt;team is highly satisfied&lt;/strong&gt;: all people in the team consider it a good/great choice and enjoys working with it, being the verbosity the only drawback.&lt;/p&gt;

&lt;p&gt;Let us share a bit on how we do it:&lt;/p&gt;

&lt;h2&gt;
  
  
  The state management layer
&lt;/h2&gt;

&lt;p&gt;This is how our MVI base class looks like in both platforms&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;open class BaseMVIPresenter&amp;lt;S: ViewState, A: ViewAction&amp;gt; {
    public var currentViewState: S { _viewState.value! }
    public var viewState: Observable&amp;lt;S&amp;gt; { _viewState }
    fileprivate let _viewState: MutableObservable&amp;lt;S&amp;gt;

    public init(initialState: S) {
        _viewState = MutableObservable(value: initialState)
    }

    func dispatch(action: A) {
        fatalError("Must be implemented by the children")
    }
}

open class MVIPresenter&amp;lt;S: ViewState, A: ViewAction, R: ActionResult&amp;gt;: BaseMVIPresenter&amp;lt;S, A&amp;gt; {
    private var middlewares: [MVIMiddleware&amp;lt;S, A, R&amp;gt;] = []

    public func with(middleware: MVIMiddleware&amp;lt;S, A, R&amp;gt;) -&amp;gt; Self {
        middlewares.append(middleware)
        return self
    }

    open func handle(action: A, results:  @escaping (R) -&amp;gt; Void) {
        fatalError("Must be implemented by the children")
    }

    open func reduce(currentViewState: S, result: R) -&amp;gt; S {
        fatalError("Must be implemented by the children")
    }

    override public func dispatch(action: A) {
        middlewares.forEach { element in
            element.handle(action: action, presenter: self)
        }
        handle(action: action) { [weak self] result in
            Executor.execute(inBackground: false) {
                guard let self = self else { return }
                self.middlewares.forEach { middleware in
                    middleware.handle(result: result, presenter: self)
                }
                self._viewState.value = self.reduce(currentViewState: self.viewState.value!, result: result)
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
interface BaseMVIPresenter&amp;lt;ViewState, ViewAction&amp;gt; {
    val currentViewState: ViewState get() = viewState.value!!
    val viewState: Observable&amp;lt;ViewState&amp;gt;
    fun dispatch(action: ViewAction)
}

abstract class MVIPresenter&amp;lt;S : ViewState, A : ViewAction, R : ActionResult&amp;gt;(initialState: S) : BaseMVIPresenter&amp;lt;S, A&amp;gt; {
    override val viewState: Observable&amp;lt;S&amp;gt;
        get() = _viewState
    private val _viewState = MutableObservable(value = initialState)
    internal var middlewares = mutableListOf&amp;lt;MVIMiddleware&amp;lt;S, A, R&amp;gt;&amp;gt;()

    abstract fun handle(action: A, results: (R) -&amp;gt; Unit)

    abstract fun reduce(currentViewState: S, result: R): S

    override fun dispatch(action: A) {
        middlewares.forEach { element -&amp;gt;
            element.handle(action = action, presenter = this)
        }
        handle(action) { result -&amp;gt;
            Executor.execute(inBackground = false) {
                this.middlewares.forEach { middleware -&amp;gt;
                    middleware.handle(result = result, presenter = this)
                }
                this._viewState.value = this.reduce(currentViewState = this.viewState.value!!, result = result)
            }
        }
    }
}

fun &amp;lt;S : ViewState, A : ViewAction, R : ActionResult, T : MVIPresenter&amp;lt;S, A, R&amp;gt;&amp;gt; T.with(middleware: MVIMiddleware&amp;lt;S, A, R&amp;gt;): T {
    middlewares.add(middleware)
    return this
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: In MVI there is no definition whether the data management part should be done in a presenter, view model or whatever. However, in our case we opted to call them "Presenters" to be more inlined with the rest of the app, but they are in practice maintaining state as classic ViewModel in Android.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then, when building a feature, we need to provide the implementation of 2 methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;handle&lt;/code&gt;: This method takes the actions triggered by some other component (normally the view) and &lt;strong&gt;handles the side effects&lt;/strong&gt;. It emits new events (called "action results") with the results of the effects, like for example a network call. It does not perform any state management or manipulation, it just emits new result events.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;reduce&lt;/code&gt;: Given a state and an action result, this method &lt;strong&gt;computes the next state of the view&lt;/strong&gt;. Note that it behaves as a pure function, with no side effects.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6wvqq48e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3fjncta1jpx6x09j1g11.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6wvqq48e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3fjncta1jpx6x09j1g11.png" alt="MVI diagram" width="523" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In contrast with other simpler implementations of MVI, we opted to split the code into these 2 methods to &lt;strong&gt;isolate side effects from state manipulation&lt;/strong&gt;, which make our tests much simpler and our overall solution more robust and clean. An added benefit is that there are &lt;strong&gt;no race conditions&lt;/strong&gt; possible like in other architectures, since all calls to &lt;code&gt;reduce&lt;/code&gt; are executed in serial with no partial updates possible.&lt;/p&gt;

&lt;p&gt;In addition, we added an extra piece around the presenters, called &lt;code&gt;Middleware&lt;/code&gt;, that are capable of &lt;strong&gt;reacting to events without doing state management&lt;/strong&gt;. For example, we can add all analytics tracking into a middleware or all navigation actions. This way, our presenter stays purist, just doing the state management part, and we have a set of small middlewares with a single other purpose, making it once again easier to test and maintain.&lt;/p&gt;

&lt;p&gt;Lastly, you can see how both platform implementations are quite similar, and they both avoid the usage of platform specific APIs like Combine or Flows (although they are used internally) to maximize code reusal when transpiling and also reduce the learning curve.&lt;/p&gt;

&lt;p&gt;An example Presenter would look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;internal class LessonDetailPresenter(...): MVIPresenter&amp;lt;LessonDetailState, LessonDetailAction, LessonDetailResult&amp;gt;(LessonDetailState.none) {

    override fun handle(action: LessonDetailAction, results: (LessonDetailResult) -&amp;gt; Unit) {
        when (action) {
            is LessonDetailAction.onAppear -&amp;gt; loadLesson(results, lessonId)            
            is LessonDetailAction.tapConfirmCancelEnrollment -&amp;gt; unregisterFromLesson(results)
            is LessonDetailAction.resendConfirmation -&amp;gt; resendConfirmation(results)
            ...
        }
    }

    override fun reduce(currentViewState: LessonDetailState, result: LessonDetailResult): LessonDetailState {
        return when (result) {
            is LessonDetailResult.lessonLoading -&amp;gt; LessonDetailState.loading
            is LessonDetailResult.lessonLoaded -&amp;gt; LessonDetailState.detail(sections = result.lesson.mapToLessonDetail(me = userId))
            ...
        }
    }

    private fun loadLesson(results: (LessonDetailResult) -&amp;gt; Unit, lesson: LessonId) {
        results(LessonDetailResult.lessonLoading)
        activityService.fetchLesson(lessonId)
            .then { results(LessonDetailResult.lessonLoaded(it)) }
            .catchError { results(LessonDetailResult.loadLessonByIdFailed(error)) }
        }
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And some associated middleware for navigation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;internal class LessonDetailNavigator(...) : MVIMiddleware&amp;lt;LessonDetailState, LessonDetailAction, LessonDetailResult&amp;gt;() {

    override fun handle(action: LessonDetailAction, presenter: MVIPresenter&amp;lt;LessonDetailState, LessonDetailAction, LessonDetailResult&amp;gt;) {
        when (action) {
            LessonDetailAction.tapOpenMaps -&amp;gt; openMaps(presenter = presenter)
            LessonDetailAction.tapAddToCalendar -&amp;gt; addLessonToCalendar(presenter = presenter)
            ...
        }
    }

    override fun handle(result: LessonDetailResult, presenter: MVIPresenter&amp;lt;LessonDetailState, LessonDetailAction, LessonDetailResult&amp;gt;) {
        when (result) {
            is LessonDetailResult.loadLessonByIdFailed -&amp;gt; dismiss()
            else -&amp;gt; {}
        }
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Our view layer
&lt;/h2&gt;

&lt;p&gt;Then, our views basically just receive 2 parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public struct LessonDetailView: View {
    @ObservedObject var state: ObservableViewState&amp;lt;LessonDetailState&amp;gt;
    let dispatcher: (LessonDetailAction) -&amp;gt; Void

    public var body: some View {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Composable
private fun LessonDetailView(
    viewState: LiveData&amp;lt;LessonDetailState&amp;gt;,
    dispatcher: (LessonDetailAction) -&amp;gt; Unit
) {
    ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, we are making use of &lt;a href="https://developer.android.com/jetpack/compose/state#state-hoisting"&gt;state hoisting&lt;/a&gt; for encapsulating the presenter injection (from the view layer, it does not know what class is behind the state management). This also makes our &lt;strong&gt;code much more reusable&lt;/strong&gt;, and very &lt;strong&gt;easy to setup the previews&lt;/strong&gt;, since we do not need to mock any data, service or presenter, just passing the view state down is enough. For this state hoisting we are making use of a parent &lt;code&gt;UIViewController&lt;/code&gt;/&lt;code&gt;Fragment&lt;/code&gt;, since our app is now a mixed app with only part of the views in SwiftUI/Compose. They look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;final class LessonDetailViewViewController: SwiftUIViewController&amp;lt;LessonDetailState, LessonDetailAction, LessonDetailView&amp;gt; {
    override func contentView(
        viewState: ObservableViewState&amp;lt;LessonDetailState&amp;gt;,
        dispatcher: @escaping (LessonDetailAction) -&amp;gt; Void
    ) -&amp;gt; LessonDetailView {
        LessonDetailView(state: viewState, dispatch: dispatcher)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class LessonDetailFragment : ComposeFragment&amp;lt;LessonDetailState, LessonDetailAction&amp;gt;() {
    @Composable
    override fun ContentView(
        viewState: LiveData&amp;lt;LessonDetailState&amp;gt;,
        dispatcher: (LessonDetailAction) -&amp;gt; Unit
    ) = LessonDetailView(viewState = viewState, dispatcher = dispatcher)    
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where the &lt;code&gt;SwiftUIViewController&lt;/code&gt; and &lt;code&gt;ComposeFragment&lt;/code&gt; are base classes that inject the presenter and create the &lt;code&gt;UIHostingController&lt;/code&gt;/&lt;code&gt;ComposeView&lt;/code&gt; for using SwiftUI/Compose inside with the content returned by the concrete &lt;code&gt;contentView&lt;/code&gt; method on each case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;There are tons of options and architectures to use with SwiftUI/Combine. In Playtomic, we opted for a MVI version where we have a clear separation of  the different responsibilities, single source of truth and a simple and unidirectional data flow. It also allows for very simple views and easy transpilation between platforms, with &lt;br&gt;
only one drawback so far: the extra boilerplate needed.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.godaddy.com/engineering/2021/11/05/android-state-management-mvi/"&gt;GoDaddy Studio’s Journey with State Management&lt;/a&gt;: Great article explaining some of the issues they found with MVP, MVVM and MVI in its simpler form. We find ourselves very aligned with their journey.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mvi</category>
      <category>architecture</category>
      <category>ios</category>
      <category>android</category>
    </item>
    <item>
      <title>Automating rollout releases in Android</title>
      <dc:creator>Angel G. Olloqui</dc:creator>
      <pubDate>Sat, 01 Jan 2022 10:14:06 +0000</pubDate>
      <link>https://dev.to/playtomic/automating-rollout-releases-in-android-1968</link>
      <guid>https://dev.to/playtomic/automating-rollout-releases-in-android-1968</guid>
      <description>&lt;p&gt;As part of our new release process (weekly releases) we are also &lt;strong&gt;changing the way we are publishing the apps for users&lt;/strong&gt;. Since it is now automatic, it is crucial for us to have a &lt;strong&gt;phased released&lt;/strong&gt; in which only a small subset of our users get the latest build, increasing daily and acting as a "failsafe" in case of an important bug makes it into production. &lt;/p&gt;

&lt;p&gt;For iOS, we can set the &lt;a href="https://help.apple.com/app-store-connect/#/dev3d65fcee1"&gt;Phased Release&lt;/a&gt; + Publication Date option and the AppStore will handle it in your behalf, starting the release on a certain date and increasing the automatic updates to 1%, 2%, 5%... each day.&lt;/p&gt;

&lt;p&gt;However, Google's approach is different. They do not offer a release date nor an automated phased release. Instead, they offer you with an &lt;a href="https://developers.google.com/android-publisher/api-ref/rest/v3/edits.tracks"&gt;API&lt;/a&gt; (and web dashboard) where you can set the percentage of users yourself at any time. This is in many senses much better than the Apple one, especially since this actually controls the releases and not just the automatic updates like in Apple, but it has a downside: &lt;strong&gt;it is all manual&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With our current setup of weekly releases and a phased released across 6 days (2%, 5%, 10%, 20%, 50%, 100%), this basically means having to &lt;strong&gt;update every single day the rollout amount manually&lt;/strong&gt;. I am not sure about you, but having to enter every single day to click some button is the last thing I want to do.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/ZWbeEcbeo0cKI/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/ZWbeEcbeo0cKI/giphy.gif" alt="Press the button" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating the release rollout
&lt;/h2&gt;

&lt;p&gt;So, we wondered, if we have an API to control the rollout percentage, isn't that enough to make it automatic? what if we have a CI job that run every day and basically checks if there is an ongoing rollout release, and in that case increases the percentage? Let's see how we did it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# rollout_update.py

import copy
import sys
import httplib2
from apiclient.discovery import build
from oauth2client.service_account import ServiceAccountCredentials
from oauth2client.client import AccessTokenRefreshError

TRACK = ('production')

# To run: rollout_update package_name json_credentials_path
def main():
  PACKAGE_NAME = sys.argv[1]
  credentials = ServiceAccountCredentials.from_json_keyfile_name(
    sys.argv[2],
    scopes='https://www.googleapis.com/auth/androidpublisher')

  http = httplib2.Http()
  http = credentials.authorize(http)

  service = build('androidpublisher', 'v3', http=http)

  try:
    edit_request = service.edits().insert(body={}, packageName=PACKAGE_NAME)
    result = edit_request.execute()
    edit_id = result['id']

    track_result = service.edits().tracks().get(editId=edit_id, packageName=PACKAGE_NAME, track=TRACK).execute()
    old_result = copy.deepcopy(track_result)

    print("Current status: ", track_result)
    for release in track_result['releases']:
        if 'userFraction' in release:
            rolloutPercentage = release['userFraction']
            if rolloutPercentage == 0:
                print('Release not rolled out yet')
                continue
            elif rolloutPercentage &amp;lt; 0.02:
                release['userFraction'] = 0.02                         
            elif rolloutPercentage &amp;lt; 0.05:
                release['userFraction'] = 0.05
            elif rolloutPercentage &amp;lt; 0.1:
                release['userFraction'] = 0.1
            elif rolloutPercentage &amp;lt; 0.2:
                release['userFraction'] = 0.2
            elif rolloutPercentage &amp;lt; 0.5:
                release['userFraction'] = 0.5
            elif rolloutPercentage &amp;lt; 1.0:
                del release['userFraction']
                release['status'] = 'completed'
            else:
                print('Release already fully rolled out')
                continue        
    if old_result != track_result:
        completed_releases = list(filter(lambda release: release['status'] == "completed", track_result['releases']))
        if len(completed_releases) == 2:
            track_result['releases'].remove(completed_releases[1])

        print("Updating status: ", track_result)
        service.edits().tracks().update(
                    editId=edit_id,
                    track=TRACK,
                    packageName=PACKAGE_NAME,
                    body=track_result).execute()
        commit_request = service.edits().commit(editId=edit_id, packageName=PACKAGE_NAME).execute()
        print('Edit ', commit_request['id'], ' has been committed')    


  except AccessTokenRefreshError:
      raise SystemExit('The credentials have been revoked or expired, please re-run the application to re-authorize')

if __name__ == '__main__':
  main()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to run this step, you need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get a Google Developer Service JSON key credentials file. If you have being using some automation tools like Fastlane for uploading the APK to the GooglePlay, you have this already. Otherwise, follow the instructions from &lt;a href="https://docs.fastlane.tools/actions/supply/"&gt;Fastlane Supply setup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install &lt;code&gt;pipenv&lt;/code&gt; or some dependency manager for python since the script uses &lt;code&gt;google-api-python-client&lt;/code&gt; and &lt;code&gt;oauth2client&lt;/code&gt;. You could get them by running:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipenv install google-api-python-client                            
pipenv install oauth2client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Run the script:
&lt;code&gt;pipenv run rollout_update.py &amp;lt;your_package&amp;gt; &amp;lt;json_credential_path&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The script will do the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new &lt;a href="https://developers.google.com/android-publisher/edits"&gt;Google Edit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Fetch the production track release info&lt;/li&gt;
&lt;li&gt;For each release, if it has a rollout in progress, then it increases the rollout percentage to the next "step", where steps are: 2%, 5%, 10%, 20%, 50%, 100%.&lt;/li&gt;
&lt;li&gt;If changes performed, then commit the Edit&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Connecting the CI
&lt;/h2&gt;

&lt;p&gt;So now that we have the script to increase the rollout, all we need is to schedule it. In our case, we are using &lt;a href="https://www.bitrise.io/"&gt;Bitrise&lt;/a&gt;, so we decided to schedule a workflow that runs the script every night. We even created a &lt;a href="https://github.com/angelolloqui/bitrise-step-google-play-rollout-update"&gt;Bitrise step&lt;/a&gt; in case you want to use it that handles the dependencies and running the script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  update_rollout:
    steps:
    - git::https://github.com/angelolloqui/bitrise-step-google-play-rollout-update.git@master:
        inputs:
        - package_name: com.playtomic
        - service_account_json_key_path: "$BITRISEIO_BITRISEIO_GOOGLEPLAY_SERVICE_ACCOUNT_JSON_URL"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: Your credentials file should be stored somewhere secured, like the Generic File Sorage of Bitrise&lt;/p&gt;

&lt;h2&gt;
  
  
  Halting a failing release?
&lt;/h2&gt;

&lt;p&gt;If a release goes wrong, all you need to do is to go to the Google Dashboard and halt the release as you would normally do with a manually controlled phased release. The script will simply detect the release is halted and will just ignore it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about Hotfixes?
&lt;/h2&gt;

&lt;p&gt;Well, the script makes no assumption on the type of build or release, so it will basically just work for any release, including hofixes. However, normally in hotfix builds, since they tend to be critical (otherwise we would not make a hotfix but wait for next week build) we normally deploy it to 100% of the user base, so we do not really need this step to run.&lt;/p&gt;

</description>
      <category>android</category>
      <category>googleplay</category>
      <category>automation</category>
      <category>bitrise</category>
    </item>
    <item>
      <title>Keyboard observer in Android with LiveData</title>
      <dc:creator>Angel G. Olloqui</dc:creator>
      <pubDate>Mon, 23 Mar 2020 09:52:41 +0000</pubDate>
      <link>https://dev.to/playtomic/keyboard-observer-in-android-with-livedata-o4h</link>
      <guid>https://dev.to/playtomic/keyboard-observer-in-android-with-livedata-o4h</guid>
      <description>&lt;p&gt;It is very common in mobile apps to need to update UI when keyboard is shown or dismissed. It might be to make some extra space in the screen, to scroll a list to a particular position or just to show some hints to the user while typing. Nevertheless, no matter what your reason is, there is no easy way to detect keyboard opening or closing in Android. Let's explore some ideas:&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: Focus listener ❌
&lt;/h3&gt;

&lt;p&gt;A first approach would be to &lt;strong&gt;detect a focus changes on a particular &lt;code&gt;EditText&lt;/code&gt;&lt;/strong&gt;. Code looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;editText.setOnFocusChangeListener { _, hasFocus -&amp;gt;
    if (hasFocus) {
        keyboardOpen()
    } else {
        keyboardClosed()
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, this solution does not solve the problem completely because when the keyboard is displayed (and field has focus), &lt;strong&gt;if the user presses the phone's back button, then the keyboard is dismissed but the focus remains in the &lt;code&gt;EditText&lt;/code&gt;&lt;/strong&gt;. You may think that you can solve this by overriding and detecting &lt;code&gt;onKeyDown&lt;/code&gt; or &lt;code&gt;onBackPressed&lt;/code&gt; on your activity, but in fact the events are not sent to your activity and there is no way to either detect the dismissal or to force the &lt;code&gt;EditText&lt;/code&gt; to lose the focus with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: ViewTree listener ⚠️
&lt;/h3&gt;

&lt;p&gt;A second approach would be add a &lt;strong&gt;global view tree layout listener&lt;/strong&gt;, so every time you have a layout change then you can compute the height difference between your activity root view and the visible display frame for it. Code looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
    val displayRect = Rect().apply { contentView.getWindowVisibleDisplayFrame(this) }
    val keypadHeight = contentView.rootView.height - displayRect.bottom
    if (keypadHeight &amp;gt; minKeyboardHeight) {
        keyboardOpen()
    } else {
        keyboardClosed()
    }
}
contentView.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Althought this solution works, it is quite cumbersome and leaves up to the developer big responsibilities like &lt;strong&gt;removing the observer when fragment/activity is destroyed&lt;/strong&gt;. Besides, if you need to do this in many different views, you will end up with &lt;strong&gt;a lot of repetition&lt;/strong&gt; of code that is not trivial. Lastly, the global layout listener will be fired multiples times, so your &lt;code&gt;keyboardOpen&lt;/code&gt; and &lt;code&gt;keyboardClosed&lt;/code&gt; methods will be fired lots of times with no need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution: LiveData + ViewTree listener ✅
&lt;/h3&gt;

&lt;p&gt;If we iterate on &lt;em&gt;Option 2&lt;/em&gt;, we can see that the layout listener can in fact be encapsulated in a component. If we extend that idea, and we make use of &lt;code&gt;LiveData&lt;/code&gt;, we can come with a very elegant solution. &lt;/p&gt;

&lt;p&gt;Create a small behavior class encapsulating the logic in a &lt;code&gt;LiveData&lt;/code&gt; subclass&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;open class KeyboardTriggerBehavior(activity: Activity, val minKeyboardHeight: Int = 0) : LiveData&amp;lt;KeyboardTriggerBehavior.Status&amp;gt;() {
    enum class Status {
        OPEN, CLOSED
    }

    val contentView = activity.findViewById&amp;lt;View&amp;gt;(android.R.id.content)

    val globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
        val displayRect = Rect().apply { contentView.getWindowVisibleDisplayFrame(this) }
        val keypadHeight = contentView.rootView.height - displayRect.bottom
        if (keypadHeight &amp;gt; minKeyboardHeight) {
            setDistinctValue(Status.OPEN)
        } else {
            setDistinctValue(Status.CLOSED)
        }
    }

    override fun observe(owner: LifecycleOwner, observer: Observer&amp;lt;in Status&amp;gt;) {
        super.observe(owner, observer)
        observersUpdated()
    }

    override fun observeForever(observer: Observer&amp;lt;in Status&amp;gt;) {
        super.observeForever(observer)
        observersUpdated()
    }

    override fun removeObservers(owner: LifecycleOwner) {
        super.removeObservers(owner)
        observersUpdated()
    }

    override fun removeObserver(observer: Observer&amp;lt;in Status&amp;gt;) {
        super.removeObserver(observer)
        observersUpdated()
    }

    private fun setDistinctValue(newValue: KeyboardTriggerBehavior.Status) {
        if (value != newValue) {
            value = newValue
        }
    }

    private fun observersUpdated() {
        if (hasObservers()) {
            contentView.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
        } else {
            contentView.viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutListener)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then use it everywhere just like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    // Some Fragment, but it works equally on an activity
    private var keyboardTriggerBehavior: KeyboardTriggerBehavior? = null
    ...
    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        val activity = this.activity ?: return
        keyboardTriggerBehavior = KeyboardTriggerBehavior(activity).apply {
            observe(viewLifecycleOwner, Observer {
                when (it) {
                    KeyboardTriggerBehavior.Status.OPEN -&amp;gt; keyboardOpen()
                    KeyboardTriggerBehavior.Status.CLOSED -&amp;gt; keyboardClosed()
                }
            })
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a number of benefits of this solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is highly reusable&lt;/strong&gt;, you can include this in any of your views very easily&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It automatically detects the lifecycle events&lt;/strong&gt; thanks to the usage of &lt;code&gt;LiveData&lt;/code&gt;, which means that it is secure to use since it will remove listeners automatically when your fragment/activity is destroyed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It behaves like any other observable&lt;/strong&gt; of your ViewModels, and it can even be passed down to the view's databinding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It will just fire updates when there is a keyboard state change&lt;/strong&gt; and not when any layout happens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We have even added a &lt;code&gt;minKeyboardHeight&lt;/code&gt; property in case you need to use this in a splitted view.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/3o7TKP9ln2Dr6ze6f6/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/3o7TKP9ln2Dr6ze6f6/giphy.gif" alt="Magic!" width="400" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>livedata</category>
      <category>composition</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Playtomic's chat solution with Firebase Realtime DB</title>
      <dc:creator>Angel G. Olloqui</dc:creator>
      <pubDate>Mon, 24 Jun 2019 09:45:08 +0000</pubDate>
      <link>https://dev.to/playtomic/playtomic-s-chat-solution-with-firebase-realtime-db-e8p</link>
      <guid>https://dev.to/playtomic/playtomic-s-chat-solution-with-firebase-realtime-db-e8p</guid>
      <description>&lt;p&gt;A few weeks ago we posted a question in our &lt;a href="https://dev.to/playtomic"&gt;Playtomic dev blog&lt;/a&gt; and our developer network asking for chat solutions. Comments were broad, and we finally opted for a combination of &lt;a href="https://firebase.google.com" rel="noopener noreferrer"&gt;Firebase&lt;/a&gt; + &lt;a href="https://github.com/MessageKit/MessageKit" rel="noopener noreferrer"&gt;MessageKit&lt;/a&gt;/&lt;a href="https://github.com/stfalcon-studio/ChatKit" rel="noopener noreferrer"&gt;ChatKit&lt;/a&gt;. But let me make a quick recap of the different solutions available for each of the components (backend + apps):&lt;/p&gt;

&lt;h2&gt;
  
  
  Existing solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Backend
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Chat specialized backends
&lt;/h4&gt;

&lt;p&gt;Products like &lt;a href="https://www.twilio.com/chat" rel="noopener noreferrer"&gt;Twilio&lt;/a&gt;, &lt;a href="https://sendbird.com/" rel="noopener noreferrer"&gt;Sendbird&lt;/a&gt; or &lt;a href="https://pusher.com/chatkit" rel="noopener noreferrer"&gt;Pusher&lt;/a&gt; offer a chat based API and set of libraries to handle the chat connection, messages, online presence, etc. &lt;/p&gt;

&lt;h4&gt;
  
  
  Synched Cloud Databases
&lt;/h4&gt;

&lt;p&gt;In parallel to chat backend solutions there are also some general purpose online databases with real time synching that allow you to implement any feature, including chats, with your own data model. Some examples are &lt;a href="https://firebase.google.com/products/realtime-database" rel="noopener noreferrer"&gt;Firebase Realtime DB&lt;/a&gt;, &lt;a href="https://firebase.google.com/products/firestore" rel="noopener noreferrer"&gt;Firestore&lt;/a&gt;, &lt;a href="https://realm.io/products/realm-platform" rel="noopener noreferrer"&gt;Realm Platform&lt;/a&gt; or &lt;a href="https://aws.amazon.com/appsync/" rel="noopener noreferrer"&gt;AppSync&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Own Service
&lt;/h4&gt;

&lt;p&gt;You can always create your own chat server infrastructure using some real time transport technology such us WebSockets or XMPP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend / Mobile apps:
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Open source UI components
&lt;/h4&gt;

&lt;p&gt;You can use existing chat libraries for your UI instead of implementing your own. Two of the most popular and mature options are &lt;a href="https://github.com/MessageKit/MessageKit" rel="noopener noreferrer"&gt;MessageKit&lt;/a&gt; for iOS and &lt;a href="https://github.com/stfalcon-studio/ChatKit" rel="noopener noreferrer"&gt;ChatKit&lt;/a&gt; for Android. They provide most UI components you would expect from a chat.&lt;/p&gt;

&lt;h4&gt;
  
  
  Own UI
&lt;/h4&gt;

&lt;p&gt;Chat screens are, in general, not that complex from a UI perspective. As such, you could decide to implement your own views and handle text messages, images, locations,... yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full product
&lt;/h3&gt;

&lt;p&gt;Lastly, there are also some products like &lt;a href="https://chatsdk.co/" rel="noopener noreferrer"&gt;ChatSDK&lt;/a&gt;, &lt;a href="https://www.cometchat.com/" rel="noopener noreferrer"&gt;Cometchat&lt;/a&gt;, &lt;a href="http://www.chat21.org/" rel="noopener noreferrer"&gt;Chat21&lt;/a&gt; or &lt;a href="https://chatcamp.io/" rel="noopener noreferrer"&gt;Chatcamp.io&lt;/a&gt; that provide a fully featured solution, from backend to frontend. They normally provide a mobile SDK with full UI ready to be added to your project in a very quick and easy way, together with some degree of UI customization and public APIs to manage the data. &lt;/p&gt;

&lt;h2&gt;
  
  
  Solution choice for Playtomic
&lt;/h2&gt;

&lt;p&gt;For &lt;a href="https://playtomic.io" rel="noopener noreferrer"&gt;Playtomic&lt;/a&gt;, we wanted a solution that fulfills our needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Due to the nature of our app we have many thousands of “connected” users, but where most of them are just using the app with a different purpose than chatting. However, since every app user can potentially have unread messages, they will all need to connect to check for unreads, even if in the vast moyority of them will have none. Therefore, we need a solution where we can have thousands of readers per month with a very small amount of writers in a price efficient way, preferably within a free tier.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Customization&lt;/strong&gt;: We started the chat as an experiment, but we foresee a future where the chat can enable much more complex interactions that just writing/reading messages. For example, after a match, we could use the chat to suggest users to rematch, rate the other players, fill in the results, suggest similar open matches… Choosing a solution that enables us to customize messages and program rich bot interactions will be important if the experiment succeeds&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Complexity&lt;/strong&gt;: As mentioned, our chat feature is an experiment, a new channel for players to communicate with each other within the app, but it is far from our product’s core. For that reason, it was important to have an MVP in reasonable development time (1 sprint / 2 weeks) and with low operational cost.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We started exploring ChatSDK as it provided what it looked like a good balance between &lt;strong&gt;pricing&lt;/strong&gt; (Firebase Realtime DB is used as backed, which has a decent free tier), &lt;strong&gt;customization&lt;/strong&gt; (being open source and a general purpose DB on backend allows for any change needed) and &lt;strong&gt;complexity&lt;/strong&gt; (full product, backend and frontend, should be quick to integrate). However, after a couple of days integrating it in our existing app we found some issues (crashes, bad designed code, difficulties customizing appearance, platform differences,...) which made us pivot to what it is now our current setup: &lt;a href="https://firebase.google.com/products/realtime-database" rel="noopener noreferrer"&gt;Firebase Realtime DB&lt;/a&gt; + &lt;a href="https://github.com/MessageKit/MessageKit" rel="noopener noreferrer"&gt;MessageKit&lt;/a&gt; / &lt;a href="https://github.com/stfalcon-studio/ChatKit" rel="noopener noreferrer"&gt;ChatKit&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Model + Firebase Functions
&lt;/h3&gt;

&lt;p&gt;When using Firebase RealtimeDB (RTDB) you have to model your data with some constraints in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Access control&lt;/strong&gt;: RTDB provides some granular access control rules, where you can specify restrictions using user’s auth information, accessing fields on child nodes, parents or siblings, etc. However, note that it does not work from &lt;code&gt;bottom -&amp;gt; top&lt;/code&gt; but the other way &lt;code&gt;top -&amp;gt; bottom&lt;/code&gt;. This means that if you have a rule granting access to, for example, &lt;code&gt;/threads/&lt;/code&gt; to user A, even if you make another rule inside &lt;code&gt;/thread/thread-1&lt;/code&gt; denying it, the user A will still be able to fetch it since it has access to the parent node. This restriction is especially important when you consider the data model, since it forces you to split your data in different collections if you want to provide a fine grained access control like private chats while still be able to fetch all public threads for example.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Querying&lt;/strong&gt;: RTDB has some basic sorting and filtering functions, but very far from what anyone would expect for a database. In particular, you can filter or sort on a concrete value and paginate results with limit options, but you can not make complex queries combining multiple fields or expressions. Similarly to access control, this limitation can be minimized by denormalizing your model into one that already fits your filtering like, for example, creating user indexes, public indexes,...&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data Transfer&lt;/strong&gt;: One of the most costly mistakes when using RTDB is not caring about transferring. Remember that you can not make complex queries, aggregations, retrieve partial documents or restrict access on a child level. As a result, if you would access to something like &lt;code&gt;/threads&lt;/code&gt; to count the amount of existing threads, you would download the whole tree content and therefore consume enormous amounts of data on your clients (which will be later accordingly charged by Firebase). Once again, denormalization is normally the way to go to create partial documents, precalculated aggregations, user indexes, etc.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F6px1vhpe76rx56m69onr.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F6px1vhpe76rx56m69onr.jpg" alt="Denormalization everywhere"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We based our data model on the one provided by &lt;a href="https://chatsdk.co/" rel="noopener noreferrer"&gt;ChatSDK&lt;/a&gt;, with a few modifications to solve the restrictions mentioned above. Our final data model looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/devices&lt;/code&gt;: Here we store information of push notification tokens per device and per user&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/messages/{threadId}&lt;/code&gt;: We have our messages in its own index to allow low level access control per thread and to permit consumption of threads without downloading all the message list (to show user’s thread list UI)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/threads&lt;/code&gt;: Here we store threads metadata like the name, users, last message,... It is our “main” document for threads and used by the denormalization processes. It has restricted access control on a per thread level, and it is the one modified by our backend when chat updates like new players joining a match happens.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/user-threads/{userId}&lt;/code&gt;: This is our main entry point for all our users. It consists of a denormalized index of the /threads collection containing only those that are “viewable” by the user. Each time there is a new thread or an update on an existing thread under /threads, there is a denormalization function making a “copy” on all participants indexes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/users/{userId}&lt;/code&gt;: In a similar way than user-threads, this denormalized index contains information like the unread count or the user’s online presence. Once again, when there is a new message on the /messages collection, all users in the affected thread get their unread count increased (unless they are online and viewing the thread). This allows us to read this only document to fetch chat status when the application starts, and configure the unread badge on the app tabbar with the proper number without consulting the whole user thread list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the bookkeeping of all the denormalization mentioned above, &lt;strong&gt;Firebase provides Functions&lt;/strong&gt;, which are basically javascript functions that can get triggered in different circumstances (like insert/delete/update of RTDB nodes) and perform actions like sending push notifications, modifying data,.... In particular, we have the following functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Create &lt;code&gt;/messages/{threadId}/{messageId}&lt;/code&gt;&lt;/strong&gt;: Triggered when new messages are written, this function is in charge of sending push notifications, copying this last message into the thread’s last message denormalization, make the denormalized copy on user-thread index and increase user aggregations for unread counts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create &lt;code&gt;/threads/{threadId}/users/{userId}&lt;/code&gt;&lt;/strong&gt;: Triggered when a new user joins a thread, it creates a denormalized copy on the user-threads index&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete &lt;code&gt;/threads/{threadId}/users/{userId}&lt;/code&gt;&lt;/strong&gt;: Triggered when a user leaves a thread, it deletes any denormalized copy from the user-threads index and refreshes the user unread count accordingly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Analytics
&lt;/h3&gt;

&lt;p&gt;After a few weeks since going live with the chat, let's quickly explore some usage analytics for the last 30 days:&lt;/p&gt;

&lt;h4&gt;
  
  
  Number of users
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;~3000 chat openings from match detail, where approximately 30% are not players (visitors)&lt;/li&gt;
&lt;li&gt;~2400 chat openings from profile&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Number of messages:
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fdluwu0ypt9fozfhgvggn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fdluwu0ypt9fozfhgvggn.png" alt="Number of messages"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~2700 total messages, &lt;/li&gt;
&lt;li&gt;~90 message per day average&lt;/li&gt;
&lt;li&gt;Peaks of almost 250 messages per day. &lt;/li&gt;
&lt;li&gt;Increasing trend&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Number of chats:
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fljril6h1e4g0rbu1hhyp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fljril6h1e4g0rbu1hhyp.png" alt="Number of chats"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;436 chats with messages. &lt;/li&gt;
&lt;li&gt;70 chats having at least 10 messages &lt;/li&gt;
&lt;li&gt;45 chats representing around half of the total messages sent.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Firebase consumption
&lt;/h3&gt;

&lt;p&gt;After explaining the current chat usage and the denormalized data model used to reduce the Firebase bill, let's take a look on some of the &lt;a href="https://firebase.google.com/pricing" rel="noopener noreferrer"&gt;most critical quotas&lt;/a&gt; for the last 30 days to see how we made so far:&lt;/p&gt;

&lt;h4&gt;
  
  
  Function invocations
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fxetksz4affus7cx44ws4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fxetksz4affus7cx44ws4.png" alt="Function invocations"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With a free quota of 125K/month and a price of 0.4$ per million, we are using around 5% of it and very far from paying any noticeable cost from this concept.&lt;/p&gt;

&lt;h4&gt;
  
  
  RTDB storage
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F7g44p651jbxxn6hzob2b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F7g44p651jbxxn6hzob2b.png" alt="Storage"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is probably one of the most critical, since the amount of space used can only grow with time (unless we delete old chats) and it increases faster than chat usage due to denormalization. Our current use is about 8MB, with a free tier of 1GB and 1$ per GB extra. So, right now, we are below 1% of usage, with an increasing ratio of around 5MB a month, very far as well from exceeding quota at this time but probably out of quota in a not so distant future if the chat usage continues its growing trend.&lt;/p&gt;

&lt;h4&gt;
  
  
  RTDB transfer
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fc1r515peh6eaham0te35.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fc1r515peh6eaham0te35.png" alt="Download transfer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Download traffic is the most worrying quota and by far the most abused one by other projects, in some cases causing billings of &lt;a href="https://hackernoon.com/how-we-spent-30k-usd-in-firebase-in-less-than-72-hours-307490bd24d" rel="noopener noreferrer"&gt;many thousands dollars per month&lt;/a&gt;. However, in our case, with all the denormalization we made, we have used 800Mb out of the 10Gb/month free tier. This leaves us with room to grow about 10x within the free quota and with a potential cost of 1$/GB after that limit. We expect to exceed the quota in a future, but hopefully keeping the cost relatively low.&lt;/p&gt;

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

&lt;p&gt;So far we are &lt;strong&gt;very satisfied&lt;/strong&gt; with the results. The combination of Firebase RTDB + open source UI kits, allowed us to build a &lt;strong&gt;customizable solution&lt;/strong&gt; within our &lt;strong&gt;2 week development&lt;/strong&gt; timebox. Besides, chats are behaving nicely so far, with &lt;strong&gt;low latency&lt;/strong&gt; and &lt;strong&gt;offline support&lt;/strong&gt;, while we are still pretty far from leaving the &lt;strong&gt;free tier&lt;/strong&gt;. Of course, we did not implement many of the advanced options that other chats provide, things like read-checks, mute chat, typing indicators,... will have to wait, but we have the basis ready to continue development if wanted. &lt;/p&gt;

&lt;p&gt;Chances are in the future we will run into other issues due to denormalization, but so far we have a decent solution in the hands of our users while we explore if the expirement is worth to continue investing on it (as it looks like so far based on usage).&lt;/p&gt;

</description>
      <category>firebase</category>
      <category>chat</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Playtomic's Shared Architecture using Swift and Kotlin</title>
      <dc:creator>Angel G. Olloqui</dc:creator>
      <pubDate>Mon, 13 Aug 2018 14:13:10 +0000</pubDate>
      <link>https://dev.to/playtomic/playtomics-shared-architecture-using-swift-and-kotlin-320b</link>
      <guid>https://dev.to/playtomic/playtomics-shared-architecture-using-swift-and-kotlin-320b</guid>
      <description>&lt;p&gt;Choosing the technology stack is one of the first and most important decisions when starting any project from scratch. At &lt;a href="https://playtomic.io" rel="noopener noreferrer"&gt;Playtomic&lt;/a&gt;, we knew that we wanted to pick a stack for the mobile apps that would allow us to deliver the best possible experience given our limited resources available.&lt;/p&gt;

&lt;p&gt;Mobile stacks range over a plethora of alternatives:&lt;/p&gt;

&lt;h4&gt;
  
  
  Web technologies
&lt;/h4&gt;

&lt;p&gt;Solutions like responsive web apps or progressive web apps allow you to leverage your web frontend experience while having a single project for all. Some native capabilities can not be used but for most apps it would be enough. Distribution is through web browsers and not inside an App Store which may be an advantage or disadvantage depending on your case&lt;/p&gt;

&lt;h4&gt;
  
  
  Hybrid
&lt;/h4&gt;

&lt;p&gt;Next step on the road to native you can choose to use an hybrid framework like Phonegap/Cordoba, which also uses web technologies but get wrapped into an app bundle, offering extra capabilities and an improved UX over pure web.&lt;/p&gt;

&lt;h4&gt;
  
  
  Multiplatform Solutions
&lt;/h4&gt;

&lt;p&gt;There are several multiplatform solutions like Xamarin, ReactNative or the newer Flutter. They all have their own selling points and disadvantages, but in general they offer a unified solution to build native apps by using a single language and, to some degree, a single UI layer with custom components, sharing most of the code across the platforms while delivering a good UX, very close (if not the same) than the one delivered by native apps.&lt;/p&gt;

&lt;h4&gt;
  
  
  Native
&lt;/h4&gt;

&lt;p&gt;The industry standard, and the one that brings the best UX is also the one that requires the most resources. Building native means building an app per platform, each one with its own language, set of tools, libraries and frameworks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our dilemma
&lt;/h2&gt;

&lt;p&gt;Without getting into too much detail about each one and our analysis, we knew that we wanted Playtomic to be a leader in the sports area, and being a mobile first project we wanted it to bring the best possible experience to our end users.&lt;/p&gt;

&lt;p&gt;We also knew that once picked, that technology stack would be our one and only one stack for long since we do not have the resource power to maintain a “Frankenstein app” built in parts with different stacks or Big Bang refactors to completely migrate from one to another. We wanted “the stack” to be production ready and with enough community and maturity to have the “certainty” that it will be supported for our project’s life.&lt;/p&gt;

&lt;p&gt;That basically left us with 3 candidates: &lt;strong&gt;Xamarin&lt;/strong&gt;, &lt;strong&gt;ReactNative&lt;/strong&gt; and &lt;strong&gt;Native&lt;/strong&gt;; and from those first two we were much more appealed by React than Xamarin because of its programming model, the possibilities to share code with a future web frontend and the amazing community and tooling.&lt;/p&gt;

&lt;p&gt;On the other hand, when selecting a solution, you also have to consider the team you have. In our case, we were expert native developers, with experience in both platforms (Android and iOS) and with little to no experience in React or other multiplatform stacks. Besides, at that moment, there was no web frontend developer or anyone within the company with enough React experience to coach the mobile team during the learning curve.&lt;/p&gt;

&lt;p&gt;Having all that in mind, &lt;strong&gt;our best fit was native&lt;/strong&gt;. It delivers almost everything we wanted (best UX, industry standard, maturity, long term vision, great tools, great community,...) except for one important aspect: &lt;strong&gt;cost&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As explained before, going native would mean to build 2 apps (iOS/Android), which has an extra cost compared to the single app of multiplatform solutions. But how much? Let’s try to put very rough numbers with an example (&lt;em&gt;note that these are not real numbers, they are just an estimate based on our own previous experience, don’t take them too seriously but just as an illustration&lt;/em&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Native&lt;/strong&gt;: Let’s say you are building a feature that takes you 100h on iOS. Then, porting it to Android would take around 80h extra (not 100 because there is always knowledge that can be “ported” from the first platform). A total of 180h&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multiplatform&lt;/strong&gt;: The same feature would take you around 120h, depending on how much you can reuse and the technology used. It is not write once run everywhere but close enough to add only a small percentage of extra work over a single platform.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, roughly 180h vs 120h, or in other words &lt;strong&gt;around 50% extra time to go native&lt;/strong&gt;. That is quite a lot! Especially for a small team like ours.&lt;/p&gt;

&lt;p&gt;So, our next question was: &lt;strong&gt;can we find a way of building a native app maximizing reusability across platforms and keeping the cost down&lt;/strong&gt;, close to the one delivered by multiplatform solutions? And if so, will it take us less than 1-2 months work of setup? (That was the time we had until the product and design teams would start delivering well defined features to build)&lt;/p&gt;

&lt;p&gt;I had participated in the past of some very small projects (Proof Of Concepts and a minor library) using this approach with excellent results. But building a full app is a completely different challenge, especially when the application grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared foundations
&lt;/h2&gt;

&lt;p&gt;So, we started building with one objective in mind: &lt;strong&gt;reusability across native platforms&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What we did was to split the app in 3 main parts for both platforms:&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;Anemone SDK&lt;/strong&gt;: framework used to connect to our backend and provide persistence. It provides Model, Service and some utilities.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Playtomic UI&lt;/strong&gt;: framework with visual components like custom textfields, input validators, generic datasources/adapters, animations, ...&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Application code&lt;/strong&gt;: where our modules and features are built. It includes and uses the former two.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We made sure that both frameworks offered the same public API (different implementations) and we also built a few facades over concepts that would be used across the app and that were provided differently on each platform, keeping the same API again. To name a few:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;Promise&lt;/code&gt; (&lt;a href="https://github.com/angelolloqui/blog-shared-architecure-swift/blob/master/promise/Promise.swift" rel="noopener noreferrer"&gt;iOS&lt;/a&gt; &lt;a href="https://github.com/angelolloqui/blog-shared-architecure-kotlin/blob/master/promise/Promise.kt" rel="noopener noreferrer"&gt;Android&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;HttpClient&lt;/code&gt; (&lt;a href="https://github.com/angelolloqui/blog-shared-architecure-swift/blob/master/http/HttpClient.swift" rel="noopener noreferrer"&gt;iOS&lt;/a&gt; &lt;a href="https://github.com/angelolloqui/blog-shared-architecure-kotlin/blob/master/http/OkHttpClient.kt" rel="noopener noreferrer"&gt;Android&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;JSONObject&lt;/code&gt; (&lt;a href="https://github.com/angelolloqui/blog-shared-architecure-swift/blob/master/json/JSONObject.swift" rel="noopener noreferrer"&gt;iOS&lt;/a&gt; &lt;a href="https://github.com/angelolloqui/blog-shared-architecure-kotlin/blob/master/json/JSONObject.kt" rel="noopener noreferrer"&gt;Android&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;NavigationManager&lt;/code&gt; (&lt;a href="https://github.com/angelolloqui/blog-shared-architecure-swift/blob/master/manager/navigation/NavigationManager.swift" rel="noopener noreferrer"&gt;iOS&lt;/a&gt; &lt;a href="https://github.com/angelolloqui/blog-shared-architecure-kotlin/blob/master/manager/navigation/NavigationManager.kt" rel="noopener noreferrer"&gt;Android&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;LocationManager&lt;/code&gt; (&lt;a href="https://github.com/angelolloqui/blog-shared-architecure-swift/blob/master/manager/location/LocationManager.swift" rel="noopener noreferrer"&gt;iOS&lt;/a&gt; &lt;a href="https://github.com/angelolloqui/blog-shared-architecure-kotlin/blob/master/manager/location/LocationManager.kt" rel="noopener noreferrer"&gt;Android&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;  ...&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;(You can check all code in these repos: &lt;a href="https://github.com/angelolloqui/blog-shared-architecure-swift" rel="noopener noreferrer"&gt;Swift&lt;/a&gt; &lt;a href="https://github.com/angelolloqui/blog-shared-architecure-kotlin" rel="noopener noreferrer"&gt;Kotlin&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We also picked the combination of Swift/Kotlin because of their enormous similarities and we used &lt;a href="https://github.com/angelolloqui/SwiftKotlin" rel="noopener noreferrer"&gt;SwiftKotlin&lt;/a&gt; to minimize the time needed to transpile code from iOS to Android.&lt;/p&gt;

&lt;p&gt;Finally, we added a few extensions over foundation objects to provide some of the missing methods on one or the other language (ex: &lt;code&gt;compactMap&lt;/code&gt;, &lt;code&gt;flatMap&lt;/code&gt;, &lt;code&gt;let&lt;/code&gt;,...)&lt;/p&gt;

&lt;h2&gt;
  
  
  Examples
&lt;/h2&gt;

&lt;p&gt;Let me go quickly through an example, in this case a Login view and presenter in Swift and Kotlin&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;See in the above screenshot how with just a minor fix on the Kotlin code we get a fully working Android version of a Presenter by transpiling the iOS one in about 15 seconds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  LoginPresenter
&lt;/h4&gt;

&lt;p&gt;Swift&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class LoginPresenter: Presenter&amp;lt;ILoginView&amp;gt; {
    let coordinator: IAuthCoordinator
    let appEventManager: IAppEventManager
    let messageBarManager: IMessageBarManager
    let navigationManager: INavigationManager
    let authenticationService: IAuthenticationService    

    init(coordinator: IAuthCoordinator,
         appEventManager: IAppEventManager,
         messageBarManager: IMessageBarManager,
         navigationManager: INavigationManager,
         authenticationService: IAuthenticationService) {
        self.coordinator = coordinator
        self.appEventManager = appEventManager
        self.messageBarManager = messageBarManager
        self.navigationManager = navigationManager
        self.authenticationService = authenticationService
    }

    override func viewPresented() {
        super.viewPresented()
        view?.setIsLoading(false)
        if authenticationService.isLoggedIn() {
            skipLogin()
        }
    }

    func skipLogin() {
        self.view.let { self.navigationManager.dismiss(view: $0, animated: true) }
    }

    func login(email: String, password: String) {
        view?.setIsLoading(true)
        authenticationService.login(user: email, password: password).then { [weak self] _ in
            guard let `self` = self else { return }
            self.view.let { self.navigationManager.dismiss(view: $0, animated: true) }
            self.appEventManager.sendEvent(AppEvent.loginWithCredentials(success: true))
        }.always { [weak self] in
            self?.view?.setIsLoading(false)
        }.catchError { [weak self] (error) in
            self?.messageBarManager.showError(error: error)
            self?.appEventManager.sendEvent(AppEvent.loginWithCredentials(success: false))
        }
    }

    func rememberPassword() {
        navigationManager.show(coordinator.requestPasswordIntent(), animation: NavigationAnimation.push)
    }

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

&lt;/div&gt;



&lt;p&gt;Kotlin&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class LoginPresenter(
        private val coordinator: IAuthCoordinator,
        private val appEventManager: IAppEventManager,
        private val messageBarManager: IMessageBarManager,
        private val navigationManager: INavigationManager,
        private val authenticationService: IAuthenticationService)
    : Presenter&amp;lt;ILoginView&amp;gt;() {

    override fun viewPresented() {
        super.viewPresented()
        view?.setIsLoading(false)
        if (authenticationService.isLoggedIn()) {
            skipLogin()
        }
    }

    fun skipLogin() {
        this.view?.let { this.navigationManager.dismiss(view = it, animated = true) }
    }

    fun login(email: String, password: String) {
        view?.setIsLoading(true)
        authenticationService.login(email, password)
                .then {
                    this.view?.let { this.navigationManager.dismiss(view = it, animated = true) }
                    appEventManager.sendEvent(AppEvent.loginWithCredentials(success = true))
                }
                .always { 
                    view?.setIsLoading(false) 
                }
                .catchError { error -&amp;gt;
                    messageBarManager.showError(error = error)
                    appEventManager.sendEvent(AppEvent.loginWithCredentials(success = false))
                }
    }

    fun rememberPassword() {
        navigationManager.show(coordinator.requestPasswordIntent(), animation = NavigationAnimation.push)
    }

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

&lt;/div&gt;



&lt;h6&gt;
  
  
  String similarity: 73.59%
&lt;/h6&gt;

&lt;h4&gt;
  
  
  LoginView
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class LoginViewController: PresenterViewController&amp;lt;LoginPresenter&amp;gt; {
    @IBOutlet weak var usernameTextField: PlaytomicTextField!
    @IBOutlet weak var passwordTextField: PlaytomicTextField!
    @IBOutlet weak var loginButton: UIButton!
    @IBOutlet weak var dismissButton: UIButton!
    @IBOutlet weak var loadingIndicator: UIActivityIndicatorView!

    override func viewDidLoad() {
        super.viewDidLoad()

        usernameTextField.configure(
            inputType: .email,
            labelText: R.string.localizable.auth_login_user_field(),
            errorMessage: R.string.localizable.auth_login_user_error(),
            validators: [
                TextFieldEmailValidatorBehavior(textField: usernameTextField.textField)
            ],
            editTextDidChangeCallback: { [weak self] in self?.reloadLoginButtonState() }
        )

        passwordTextField.configure(
            inputType: .password,
            labelText: R.string.localizable.auth_login_password_field(),
            errorMessage: R.string.localizable.auth_login_password_error(),
            validators: [
                TextFieldLengthValidatorBehavior(textField: passwordTextField.textField, minLength: 5, maxLength: nil)
            ],
            editTextDidChangeCallback: { [weak self] in self?.reloadLoginButtonState() }
        )
        reloadLoginButtonState()
    }

    @IBAction func login() {
        view.endEditing(true)
        presenter.login(email: usernameTextField.text, password: passwordTextField.text)
    }

    @IBAction func skipLogin() {
        presenter.skipLogin()
    }

    @IBAction func rememberPassword() {
        presenter.rememberPassword()
    }

    func reloadLoginButtonState() {
        let fieldsValid = usernameTextField.isValid &amp;amp;&amp;amp; passwordTextField.isValid
        let loading = loadingIndicator.isAnimating
        loginButton.isEnabled = fieldsValid &amp;amp;&amp;amp; !loading
    }

    // ****  View Interface  ****

    func setIsLoading(loading: Bool) {
        if newValue {
            loadingIndicator.startAnimating()
        } else {
            loadingIndicator.stopAnimating()
        }
        reloadLoginButtonState()
    }

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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class LoginFragment : PresenterFragment&amp;lt;LoginPresenter&amp;gt;(R.layout.login_fragment), ILoginView {

    @BindView(R.id.username_edit_text_custom)
    lateinit var usernameCustomEditText: PlaytomicTextField

    @BindView(R.id.password_edit_text_custom)
    lateinit var passwordCustomEditText: PlaytomicTextField

    @BindView(R.id.login_button)
    lateinit var loginButton: Button

    @BindView(R.id.toolbar_back_button)
    lateinit var dismissButton: ImageButton

    @BindView(R.id.loading_indicator)
    lateinit var loadingIndicator: ProgressBar

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        usernameCustomEditText.configure(
                inputType = PlaytomicTextField.InputType.email,
                labelText = R.string.auth_login_user_field,
                errorMessage = R.string.auth_login_user_error,
                validators = listOf(
                        TextFieldEmailValidatorBehavior(usernameCustomEditText.editText)
                ),
                editTextDidChangeCallback = ::reloadLoginButtonState
        )
        usernameCustomEditText.nextTextField = passwordCustomEditText

        passwordCustomEditText.configure(
                inputType = PlaytomicTextField.InputType.password,
                labelText = R.string.auth_login_password_field,
                errorMessage = R.string.auth_login_password_error,
                validators = listOf(
                        TextFieldLengthValidatorBehavior(passwordCustomEditText.editText, 5, null)
                ),
                editTextDidChangeCallback = ::reloadLoginButtonState
        )
        reloadLoginButtonState()
    }

    @OnClick(R.id.login_button)
    internal fun login() {
        hideKeyboard()
        presenter.login(email = usernameCustomEditText.text, password = passwordCustomEditText.text)
    }

    @OnClick(R.id.auth_login_forget_password_button)
    internal fun rememberPassword() {
        presenter.rememberPassword()
    }

    private fun reloadLoginButtonState() {
        val fieldsValid = usernameCustomEditText.isValid &amp;amp;&amp;amp; passwordCustomEditText.isValid
        val loading = loadingIndicator.visibility == View.VISIBLE
        loginButton.isEnabled = fieldsValid &amp;amp;&amp;amp; !loading
    }

    // ****  View Interface  ****

    override fun setIsLoading(loading: Boolean) {
        loadingIndicator.visibility = if (loading) View.VISIBLE else View.GONE
        reloadLoginButtonState()
    }

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

&lt;/div&gt;



&lt;h6&gt;
  
  
  String similarity: 70.8%
&lt;/h6&gt;




&lt;p&gt;As you can see, code is basically the same except for some language differences (constructors, keywords,...) that can be quickly transpiled. Moreover, by using PlaytomicUI components and some of our extensions, code is similar even on the View layer. The main work on this part corresponds to laying out elements in Interface Builder (iOS) or in layout XMLs (Android).&lt;/p&gt;

&lt;p&gt;An interesting note to make here is that we could  have decided to write the Views in code or with tools like &lt;a href="https://github.com/schibsted/layout" rel="noopener noreferrer"&gt;Layout&lt;/a&gt;. That would make possible to reuse much more here as well, but we intentionally chose not to because we wanted to keep this part (the one the user actually sees and experiences) as standard as possible. This also allows us to use and follow platform components and conventions when desired and the de facto developer tools available, hence keeping a moderate learning curve and taking advantage to a full extent of our native development expertise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good, the bad and the ugly
&lt;/h2&gt;

&lt;p&gt;After 1.5 years working with the explained Shared Architecture, conventions and tools, we have a pretty solid view of what’s working for us and what is not working that well. Let me try to make an introspection:&lt;/p&gt;

&lt;h3&gt;
  
  
  The good
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Team unification&lt;/strong&gt;: there is no Android/iOS team distinction because the same developer always transpiles his work to the other platform. This results in extreme union, platform parity and less disputes/blockages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team performance&lt;/strong&gt;: developing app code is much faster than writing 2 platforms independently. It typically takes just a few minutes to transpile Presenters, Interactors, Coordinators, Models and Services. XML and Xib files takes the rest of the time, and every now and then some code in managers. In average, we take about 30% extra time to convert from one to the other platform, depending on the amount and complexity of the views involved, pretty close to multiplatform solutions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fully native UX&lt;/strong&gt;: Visual components and app performance is the same than any other native app. Besides, there is no extra penalty on app size nor app launch time like in multiplatform solutions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long term vision&lt;/strong&gt;: we use the facto tools, frameworks and languages on each platform, and we have no important dependencies. We can have the certainty that code will be valid for many years, even if at some point team grows and we stop sharing code they will still be valid standard native projects independently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Good abstractions and code quality&lt;/strong&gt;: The fact that we want to reuse as much code as possible forces developers to think very carefully the abstractions they want to build. It encourages for proper separation of concerns, single responsibility classes, more testing (to verify the transpilation), etc. In fact I would even say that code reviews are also more effective as you can compare the PR side by side with the counterpart and detect issues on a higher level. Quality is not just desirable but it is also perceived as an actual productivity boost from day 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduced project’s cognitive load&lt;/strong&gt;: Having 1 code base makes understanding the project and remembering the internal details much easier.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The bad
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Extra architecture work&lt;/strong&gt;: it is no secret that building these shared abstractions and extensions take time. In our case we dedicated about 1 month to architectural foundations, and since then we have had to make some changes and additions every now and then. The total overhead is difficult to calculate, but it is noticeable especially at the beginning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden bugs from language differences&lt;/strong&gt;: transpilation works great, most of the time 💥. However, during these 18 months working on it, we have encountered 3 or 4 times bugs derived from language differences that were unexpected. Especially important is the Value type (&lt;code&gt;struct&lt;/code&gt;) in Swift that has no counterpart in Kotlin or the sort in place of arrays in Kotlin. These differences impose restrictions and are a source of bugs if not considered properly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maximum common factor of language features&lt;/strong&gt;: in parallel to the previous bullet, having to share code imposes restrictions on the way you use a language (or more transpilation work is required). As a result, we tend to write code using the maximum common factor of language features. A few examples of limitations on Swift are value types and protocol extensions, while in Kotlin the usage of decorators or default parameters in interfaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View layer needs work per platform&lt;/strong&gt;: writing views require specific work per platform. That has an impact on development time, testing and bug fixing that would not ocurr that much with multiplatform solutions with shared UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning curve&lt;/strong&gt;: all the architectural components and code conventions that we use are specific from this project and therefore require some learning. Nevertheless, to be fair all projects have their own internal conventions and architecture design, so at least having the same across both platforms means that there is only 1 curve to pass and not 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The ugly
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid conventions&lt;/strong&gt;: Kotlin and Swift are very similar but they use different naming conventions. For example, in Kotlin/Java constants are written in upper case while in Swift they aren’t. Or the classical &lt;code&gt;I&lt;/code&gt; prefix so common in Java does not exist in Swift (the closest would be to suffix &lt;code&gt;Protocol&lt;/code&gt; to the name). As a result, when sharing code you have to either come with a mix of conventions or penalize the transpilation process with more manual edition to adapt from one to the other. We started with conventions per platform and we are gradually moving into a single convention that feels to us like the best of both worlds and which is becoming our de facto mobile team convention (but it would look “ugly” to external developers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replicate changes manually&lt;/strong&gt;: transpilation works great when building new features because you can copy&amp;amp;paste the full transpiled code. However, when maintaining code, it is up to the developer to remember to apply the same change made on platform A into platform B. As a result, we have sometimes forgotten to replicate, resulting in some inconsistencies on app behavior. We are controlling that through PR, forcing both platforms to have the same kind of changes and reviewing them in parallel, but there is still the case for human error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team scaling&lt;/strong&gt;: having such a small team helps when using this approach since it requires lots of communication between members. We are not sure how this would scale with more developers, but we suspect it won’t the day we have 6+ people or so. Besides, we are “forced” to hire (or teach) multiplatform experts as long as we want to keep sharing code efficiently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall, when looking back, we feel the decision has been &lt;strong&gt;the correct one for our team&lt;/strong&gt;. That does not mean that using React would have been a mistake (probably not), but we are very satisfied with the results we are getting. Sure, we run into some issues every now and then, and we have had to invest a couple of months on making the abstractions (this time would have gone to learning React anyway), but &lt;strong&gt;we have now the best UX possible with a very decent development speed&lt;/strong&gt;. Moreover, we are &lt;strong&gt;not dependent on some third party framework or tool&lt;/strong&gt; (even SwiftKotlin could be removed and just transpile code manually, which is not that bad anyway) what gives us long term confidence, and we are &lt;strong&gt;free to chose the application architecture&lt;/strong&gt; we prefer per module (MVP, MVVM, VIPER, REDUX,...). We can also leverage all of the &lt;strong&gt;native goodies&lt;/strong&gt; the instant they are announced and we can use the &lt;strong&gt;team knowledge to full extent&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;String similarity calculated with &lt;a href="https://www.tools4noobs.com/online_tools/string_similarity/" rel="noopener noreferrer"&gt;Tools4Noobs&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>playtomic</category>
      <category>swift</category>
      <category>kotlin</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
