<?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: Márton Braun</title>
    <description>The latest articles on DEV Community by Márton Braun (@zsmb13).</description>
    <link>https://dev.to/zsmb13</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%2F573503%2F8c864b9c-99a5-441e-bb89-7612b1391357.png</url>
      <title>DEV Community: Márton Braun</title>
      <link>https://dev.to/zsmb13</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zsmb13"/>
    <language>en</language>
    <item>
      <title>The conflation problem of testing StateFlows</title>
      <dc:creator>Márton Braun</dc:creator>
      <pubDate>Tue, 15 Aug 2023 15:10:58 +0000</pubDate>
      <link>https://dev.to/zsmb13/the-conflation-problem-of-testing-stateflows-1ooj</link>
      <guid>https://dev.to/zsmb13/the-conflation-problem-of-testing-stateflows-1ooj</guid>
      <description>&lt;p&gt;&lt;a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/"&gt;&lt;code&gt;StateFlow&lt;/code&gt;&lt;/a&gt; has a bit of a &lt;a href="https://en.wikipedia.org/wiki/Wave%E2%80%93particle_duality"&gt;wave–particle duality&lt;/a&gt;. On one hand, it's a data holder which holds a current value. On the other hand, it's also a &lt;a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/"&gt;&lt;code&gt;Flow&lt;/code&gt;&lt;/a&gt;, emitting the values it holds over time to its collectors. Importantly, as the type's documentation states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Updates to the value are always &lt;a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/conflate.html"&gt;conflated&lt;/a&gt;. So a slow collector skips fast updates, but always collects the most recently emitted value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This conflation means that depending on how "fast" the code setting values in the &lt;code&gt;StateFlow&lt;/code&gt; and the code collecting values from it are relative to each other, the collector may or may not receive intermediate values when the &lt;code&gt;StateFlow&lt;/code&gt;'s value is rapidly updated multiple times.&lt;/p&gt;

&lt;p&gt;In production code, this generally shouldn't cause any issues. The collector should not be affected by conflation happening or not happening. The end result - for example, what's displayed on the UI - should remain the same. Testing, however, is a different story.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing StateFlows
&lt;/h3&gt;

&lt;p&gt;There are two approaches for testing a &lt;code&gt;StateFlow&lt;/code&gt;: you can either make assertions on its &lt;code&gt;value&lt;/code&gt; property, or collect it as a &lt;code&gt;Flow&lt;/code&gt; and assert on the values collected. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This article assumes that you're familiar with &lt;a href="https://developer.android.com/kotlin/coroutines/test"&gt;coroutine testing&lt;/a&gt; using the &lt;a href="https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-test"&gt;kotlinx-coroutines-test&lt;/a&gt; library.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Generally speaking, it's simpler to treat &lt;code&gt;StateFlow&lt;/code&gt; as a state holder and make assertions on its &lt;code&gt;value&lt;/code&gt; after performing actions in the test, as described in the Android documentation about &lt;a href="https://developer.android.com/kotlin/flow/test#stateflows"&gt;testing StateFlows&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, there might be cases where this is not feasible to do, and you need to collect from a &lt;code&gt;StateFlow&lt;/code&gt; in a test. For example, a single action on a class under test may trigger multiple value writes on the &lt;code&gt;StateFlow&lt;/code&gt;, and you may want to assert on all intermediate values written (instead of just asserting on &lt;code&gt;value&lt;/code&gt; once, after the last write).&lt;/p&gt;

&lt;p&gt;A typical case of this is testing a ViewModel function that first triggers a loading state and then replaces that with actual data (oversimplified pseudocode!):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyViewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MyState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Initial&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Loading&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;In this specific example, you might be able to write a test which only reads the &lt;code&gt;value&lt;/code&gt; property of the &lt;code&gt;StateFlow&lt;/code&gt;, by injecting a fake repository implementation that lets us control when &lt;code&gt;getData&lt;/code&gt; returns, giving us a chance to read &lt;code&gt;state.value&lt;/code&gt; while it's still in the loading state. However, it's often not possible or too cumbersome to control the object under test this way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Experiencing conflation
&lt;/h3&gt;

&lt;p&gt;If you choose to test a &lt;code&gt;StateFlow&lt;/code&gt; by collecting it, you'll need to take conflation into account. When writing a test asserting on values collected from a &lt;code&gt;StateFlow&lt;/code&gt;, you have to decide whether you expect to see all values without conflation, or to experience conflation and only see the most recent value after rapid updates.&lt;/p&gt;

&lt;p&gt;Let's see what happens if you start two coroutines in a test using &lt;a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-test.html"&gt;&lt;code&gt;runTest&lt;/code&gt;&lt;/a&gt;, one collecting from the &lt;code&gt;StateFlow&lt;/code&gt; and another setting values in it. In this case, they'll be equally "fast", as they'll both inherit the &lt;code&gt;StandardTestDispatcher&lt;/code&gt; from &lt;code&gt;runTest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We'll call the first coroutine the &lt;em&gt;collecting&lt;/em&gt; coroutine, and the second coroutine the &lt;em&gt;producing&lt;/em&gt; coroutine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;useStandardTestDispatcherForCollection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;runTest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;stateFlow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Collecting coroutine&lt;/span&gt;
    &lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Conflation happened&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Producing coroutine&lt;/span&gt;
    &lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As they are launched on a &lt;code&gt;StandardTestDispatcher&lt;/code&gt;, both coroutines here will first be queued up on the test scheduler. When &lt;code&gt;runTest&lt;/code&gt; reaches the end of the lambda passed to it, it will start executing the tasks queued up on the scheduler.&lt;/p&gt;

&lt;p&gt;This starts the collecting coroutine first, which begins collecting the &lt;code&gt;StateFlow&lt;/code&gt;. The &lt;code&gt;StateFlow&lt;/code&gt; immediately emits its initial value, which is collected into the list. The collecting coroutine then suspends as &lt;code&gt;toList&lt;/code&gt; waits for new values.&lt;/p&gt;

&lt;p&gt;This lets the second coroutine start, which sets the value of the &lt;code&gt;StateFlow&lt;/code&gt; several times. Setting the &lt;code&gt;value&lt;/code&gt; property of &lt;code&gt;StateFlow&lt;/code&gt; is a regular property assignment, which is not a suspending call, however its implementation does &lt;a href="https://github.com/Kotlin/kotlinx.coroutines/blob/1ccc96890d38be52d1bdcd31af5befb54804aa05/kotlinx-coroutines-core/common/src/flow/StateFlow.kt#L349"&gt;resume any collecting coroutines&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Though the collector is resumed, it's on a &lt;code&gt;StandardTestDispatcher&lt;/code&gt; and still needs a chance to dispatch in order to run its code, which it doesn't get until the test thread is yielded. That only happens on the completion of the producing coroutine, after all three value writes are done.&lt;/p&gt;

&lt;p&gt;With the completion of the producing coroutine, the collector gets to execute, and receives only the latest value, which it places in the list. Conflation happened.&lt;/p&gt;

&lt;h3&gt;
  
  
  Avoiding conflation
&lt;/h3&gt;

&lt;p&gt;Let's see what you can do if you want to avoid conflation in our tests.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that conflation is not inherently bad. However, it's often undesirable in testing scenarios that want to verify all the behaviour of an object.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h5&gt;
  
  
  Yielding manually
&lt;/h5&gt;

&lt;p&gt;For a simple approach, if you let go of the test thread after each value assignment - for example, using a simple &lt;code&gt;yield()&lt;/code&gt; call - the collecting coroutine gets a chance to dispatch, and receives all four values, eliminating conflation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;yieldingExample&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;runTest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;stateFlow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// No conflation&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn't scale very well, as it requires explicitly yielding the thread every time a new value is set in the &lt;code&gt;StateFlow&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To make things worse, in a real test this likely happens somewhere in code that's called from the test, and not within the test itself. Modifying the scheduling behaviour of production code to make testing more convenient isn't great.&lt;/p&gt;

&lt;h5&gt;
  
  
  Collecting faster
&lt;/h5&gt;

&lt;p&gt;Alternatively, you can change how "fast" the collecting coroutine is, so that it can better keep up with new values being produced. In the next example, the collecting coroutine is created using an eager &lt;code&gt;UnconfinedTestDispatcher&lt;/code&gt; while the producing coroutine keeps using the lazier &lt;code&gt;StandardTestDispatcher&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For an explanation of the scheduling of these two dispatchers, see the Android docs on &lt;a href="https://developer.android.com/kotlin/coroutines/test#testdispatchers"&gt;&lt;code&gt;TestDispatchers&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;useUnconfinedTestDispatcherForCollection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;runTest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;stateFlow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UnconfinedTestDispatcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testScheduler&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// No conflation&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this test, collection happens immediately whenever the assignment of &lt;code&gt;value&lt;/code&gt; resumes the collecting coroutine, thanks to &lt;code&gt;UnconfinedTestDispatcher&lt;/code&gt; which doesn't require a dispatch before resuming execution. Conflation is gone!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Using &lt;code&gt;UnconfinedTestDispatcher&lt;/code&gt; for the collecting coroutine also has the added benefit that the collecting coroutine is launched eagerly. This means that by the time the first &lt;code&gt;launch&lt;/code&gt; call of the test returns the first coroutine has already started executing, collected the initial value of the &lt;code&gt;StateFlow&lt;/code&gt;, and is suspended waiting for new values to be produced.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Using the &lt;a href="https://github.com/cashapp/turbine"&gt;Turbine&lt;/a&gt; library instead of collecting from the Flow yourself behaves the exact same way, as Turbine's &lt;a href="https://cashapp.github.io/turbine/docs/1.x/-turbine/app.cash.turbine/test.html"&gt;&lt;code&gt;test&lt;/code&gt;&lt;/a&gt; function also uses &lt;code&gt;UnconfinedTestDispatcher&lt;/code&gt; under the hood if you're using it with &lt;code&gt;runTest&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;useTurbineForCollection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;runTest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;stateFlow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// No conflation&lt;/span&gt;
            &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;awaitItem&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;awaitItem&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;awaitItem&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;awaitItem&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that your collecting coroutine can only go faster if the producing coroutine is on a "slow" &lt;code&gt;StandardTestDispatcher&lt;/code&gt;. If the producing coroutine runs on an &lt;code&gt;UnconfinedTestDispatcher&lt;/code&gt; and does not yield the thread at any point, conflation makes a comeback, and you have no chance of seeing those intermediate values.&lt;/p&gt;

&lt;p&gt;Here's an example demonstrating just that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;useUnconfinedTestDispatcherForCollectionAndProduction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;runTest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;stateFlow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UnconfinedTestDispatcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testScheduler&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Conflation happened&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UnconfinedTestDispatcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testScheduler&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;When testing a &lt;code&gt;StateFlow&lt;/code&gt;, you have two options: reading its &lt;code&gt;value&lt;/code&gt; at various points during the test or collecting it as a &lt;code&gt;Flow&lt;/code&gt;. The former is the easier path if your circumstances allow it. Otherwise, you'll have to keep in mind that collectors of a &lt;code&gt;StateFlow&lt;/code&gt; are affected by conflation.&lt;/p&gt;

&lt;p&gt;It's up to you to decide whether you want values to be conflated or not during a test, and set up assertions accordingly. You can control whether conflation can happen by choosing the dispatchers your code executes on during tests. Using &lt;a href="https://developer.android.com/kotlin/coroutines/test#injecting-test-dispatchers"&gt;injected dispatchers&lt;/a&gt; that you can replace with appropriate &lt;code&gt;TestDispatcher&lt;/code&gt; instances during tests is crucial for this.&lt;/p&gt;

&lt;p&gt;Finally, the only way your collecting coroutine can be "faster" than the producing coroutine and avoid conflation is if the collector is on an &lt;code&gt;UnconfinedTestDispatcher&lt;/code&gt; while the producer is on a &lt;code&gt;StandardTestDispatcher&lt;/code&gt;. Other cases—where the coroutines are equally fast or the collector is slower—will result in conflation.&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>testing</category>
      <category>stateflow</category>
      <category>coroutines</category>
    </item>
    <item>
      <title>Adventures in Tracking Upload Progress With OkHttp and Retrofit</title>
      <dc:creator>Márton Braun</dc:creator>
      <pubDate>Fri, 10 Dec 2021 19:19:09 +0000</pubDate>
      <link>https://dev.to/zsmb13/adventures-in-tracking-upload-progress-with-okhttp-and-retrofit-1gff</link>
      <guid>https://dev.to/zsmb13/adventures-in-tracking-upload-progress-with-okhttp-and-retrofit-1gff</guid>
      <description>&lt;p&gt;This article tells the story of how Stream’s Android team refined our progress tracking process during file uploads in the &lt;a href="https://github.com/GetStream/stream-chat-android/"&gt;Stream Chat Android SDK&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Our original implementation to track file upload progress worked, but it had some in-code usability and UX issues that we wanted to clean up. &lt;/p&gt;

&lt;p&gt;The following account gives an up-close look into the process we had, the problems we encountered, and what we did to improve. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; As this is a story of mistakes we made and then corrected over time, &lt;strong&gt;do not&lt;/strong&gt; use any of the initial or intermediate forms of code here in your own projects, only the final fixed version. 😉&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Uploading Files With Retrofit
&lt;/h2&gt;

&lt;p&gt;First things first, let's see how you can upload a file to an API using &lt;a href="https://square.github.io/retrofit/"&gt;Retrofit&lt;/a&gt;. Most APIs will expect a multipart form to contain the file data. You can declare a method inside a Retrofit interface like the one below to support that operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Multipart&lt;/span&gt;
&lt;span class="nd"&gt;@POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/channels/{type}/{id}/file"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;sendFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nd"&gt;@Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;channelType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@Part&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MultipartBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;RetrofitCall&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UploadFileResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To invoke this method, you can create a &lt;code&gt;Part&lt;/code&gt; by using the &lt;code&gt;createFormData&lt;/code&gt; function, like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;sendFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;part&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MultipartBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asRequestBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMediaType&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="n"&gt;retrofitCdnApi&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channelType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you just &lt;code&gt;enqueue&lt;/code&gt; the Retrofit &lt;code&gt;Call&lt;/code&gt; to run it, and done! That's a basic, working file upload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting in the Request Body
&lt;/h2&gt;

&lt;p&gt;Time to add progress tracking. For our initial implementation, we used this callback interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ProgressCallback&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ChatError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then created a &lt;code&gt;ProgressRequestBody&lt;/code&gt; class, most likely based on &lt;a href="https://stackoverflow.com/a/33384551/4465208"&gt;this StackOverflow answer&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;This wraps a &lt;code&gt;file&lt;/code&gt; and a &lt;code&gt;callback&lt;/code&gt;, and implements the OkHttp &lt;a href="https://square.github.io/okhttp/4.x/okhttp/okhttp3/-request-body/"&gt;&lt;code&gt;RequestBody&lt;/code&gt;&lt;/a&gt; class.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProgressRequestBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ProgressCallback&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RequestBody&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;MediaType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMediaType&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;contentLength&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;writeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BufferedSink&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ByteArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DEFAULT_BUFFER_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;uploaded&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;
        &lt;span class="nc"&gt;FileInputStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;fis&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Looper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMainLooper&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;also&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;uploaded&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="n"&gt;uploaded&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLong&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;companion&lt;/span&gt; &lt;span class="k"&gt;object&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;DEFAULT_BUFFER_SIZE&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whenever &lt;code&gt;writeTo&lt;/code&gt; is invoked to write this &lt;code&gt;RequestBody&lt;/code&gt; to the network, we loop through the contents of the file manually, write the bytes, and invoke the &lt;code&gt;callback&lt;/code&gt;, calculating the percentage of the upload completed so far.&lt;/p&gt;

&lt;p&gt;This requires a modification to our API invocation, as we now have to create a &lt;code&gt;ProgressRequestBody&lt;/code&gt; that we then embed in our &lt;code&gt;Part&lt;/code&gt;, which will — remember this — contain the &lt;code&gt;ProgressCallback&lt;/code&gt; instance that was passed in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;sendFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ProgressCallback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;progressBody&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ProgressRequestBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;part&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MultipartBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;progressBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;retrofitCdnApi&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channelType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RetroProgressCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also pass the callback to the &lt;code&gt;enqueue&lt;/code&gt; call via a wrapper to adapt it to a Retrofit &lt;a href="https://square.github.io/retrofit/2.x/retrofit/retrofit2/Callback.html"&gt;&lt;code&gt;Callback&lt;/code&gt;&lt;/a&gt;, which will run the &lt;code&gt;onFailure&lt;/code&gt; and &lt;code&gt;onSuccess&lt;/code&gt; methods of our original callback as needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RetroProgressCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ProgressCallback&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Callback&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UploadFileResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UploadFileResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UploadFileResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;retrofit2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UploadFileResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;onFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"File response is null"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to explore this implementation, you can find the old version of our code &lt;a href="https://github.com/GetStream/stream-chat-android/tree/c592028765a7b541503c4a0976775cfd823c57f1/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api"&gt;here in the GitHub history&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting With a Custom Sink Implementation
&lt;/h2&gt;

&lt;p&gt;This is okay, but we can make it a lot nicer. For this change, we essentially took the implementation from &lt;a href="https://medium.com/@PaulinaSadowska/display-progress-of-multipart-request-with-retrofit-and-rxjava-23a4a779e6ba"&gt;Paulina Sadowska's post about the topic&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The improvement is to (instead of handling a &lt;code&gt;FileInputStream&lt;/code&gt; manually) create a custom OkHttp &lt;a href="https://square.github.io/okio/2.x/okio/okio/-sink/index.html"&gt;&lt;code&gt;Sink&lt;/code&gt;&lt;/a&gt; implementation (based on the handy &lt;a href="https://square.github.io/okio/2.x/okio/okio/-forwarding-sink/index.html"&gt;&lt;code&gt;ForwardingSink&lt;/code&gt;&lt;/a&gt;), which will perform the counting and corresponding progress callbacks for us. &lt;/p&gt;

&lt;p&gt;We'll also make &lt;code&gt;ProgressRequestBody&lt;/code&gt; wrap a &lt;code&gt;RequestBody&lt;/code&gt; and delegate to it whenever it needs to behave like a &lt;code&gt;RequestBody&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProgressRequestBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RequestBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ProgressCallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RequestBody&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;MediaType&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;contentLength&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contentLength&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;writeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BufferedSink&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;countingSink&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CountingSink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;countingSink&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;countingSink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;inner&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CountingSink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Sink&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ForwardingSink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Looper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMainLooper&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;contentLength&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;uploaded&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;

        &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;byteCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;byteCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;uploaded&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;byteCount&lt;/span&gt;

                        &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uploaded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this improvement, we've updated our &lt;code&gt;ProgressCallback&lt;/code&gt; interface to take the more meaningful &lt;code&gt;bytesUploaded&lt;/code&gt; and &lt;code&gt;totalBytes&lt;/code&gt; values during progress updates instead of a percentage. &lt;/p&gt;

&lt;p&gt;(We also added KDoc at this point to make the interface more obvious, which you can check out &lt;a href="https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/utils/ProgressCallback.kt"&gt;in the GitHub repo&lt;/a&gt;.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ProgressCallback&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytesUploaded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;totalBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ChatError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the call site, we can now create a simple &lt;code&gt;RequestBody&lt;/code&gt; first, and then wrap it with the &lt;code&gt;ProgressRequestBody&lt;/code&gt; implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asRequestBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMediaType&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;progressBody&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ProgressRequestBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;part&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MultipartBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;progressBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Logging Problem
&lt;/h2&gt;

&lt;p&gt;A major problem we encountered was that we had several interceptors added to the &lt;code&gt;OkHttpClient&lt;/code&gt; that we used for these uploads. The configuration looked something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;baseClientBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connectTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MILLISECONDS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MILLISECONDS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MILLISECONDS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ApiKeyInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HeadersInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TokenAuthInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpLoggingInterceptor&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
         &lt;span class="nc"&gt;CurlInterceptor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
             &lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;logI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CURL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is natural, and lots of apps have setups like this. What's notable here is that &lt;code&gt;HttpLoggingInterceptor&lt;/code&gt; and &lt;code&gt;CurlInterceptor&lt;/code&gt; will both log the request and call &lt;code&gt;requestBody.writeTo()&lt;/code&gt; internally to do that. &lt;/p&gt;

&lt;p&gt;In the case of our file upload calls, this method is what contains our progress tracking implementation.&lt;/p&gt;

&lt;p&gt;The end result is that whenever we make an upload call, we'll run the progress callback three times in a row: twice for the logging, and once when it's actually written to the network. &lt;/p&gt;

&lt;p&gt;This results in an... &lt;em&gt;interesting&lt;/em&gt; experience for the user looking at the UI, where progress goes something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="mi"&gt;37&lt;/span&gt; &lt;span class="mi"&gt;57&lt;/span&gt; &lt;span class="mi"&gt;87&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="mi"&gt;67&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;This only happened in debug builds, as we had the logging disabled in release builds, but it was still a problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fixing this wasn't simple, as we wanted to keep these logging interceptors around. After some elaboration, &lt;a href="https://github.com/GetStream/stream-chat-android/pull/2342#discussion_r709018104"&gt;we begrudgingly added an ugly, ugly workaround&lt;/a&gt;, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProgressRequestBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RequestBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RequestBody&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;writeCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;writeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BufferedSink&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writeCount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;progressUpdatesToSkip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;countingSink&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CountingSink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;countingSink&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;countingSink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;writeCount&lt;/span&gt;&lt;span class="p"&gt;++&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;companion&lt;/span&gt; &lt;span class="k"&gt;object&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;progressUpdatesToSkip&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, we simply hardcoded that the first two times when the &lt;code&gt;ProgressRequestBody&lt;/code&gt; is written, it shouldn't invoke callbacks.&lt;/p&gt;

&lt;p&gt;What made this worse is that this value wasn't actually &lt;code&gt;2&lt;/code&gt; like I assumed, because as mentioned above, we only log with these interceptors in debug builds. &lt;/p&gt;

&lt;p&gt;This meant that we had to make this a &lt;code&gt;var&lt;/code&gt; and set it dynamically. In release builds it'd be &lt;code&gt;0&lt;/code&gt;, and in debug builds, we'd increment it to &lt;code&gt;2&lt;/code&gt;. Ouch.&lt;/p&gt;

&lt;p&gt;That's bad enough, but then we also had the requirement to allow our users to set their own &lt;code&gt;OkHttpClient&lt;/code&gt; instances for the SDK to use, where they could also add more interceptors of their own, which may or may not invoke &lt;code&gt;writeTo&lt;/code&gt; in the request body (one or more times!). &lt;/p&gt;

&lt;p&gt;We could've somehow provided an additional API where they can increment &lt;code&gt;progressUpdatesToSkip&lt;/code&gt; to account for this, but then they could also have interceptors that will &lt;em&gt;sometimes&lt;/em&gt; read the body but not at other times, based on some dynamic condition... There's clearly no winning with this approach, and it's an awful rabbit hole to go down.&lt;/p&gt;

&lt;p&gt;So, to quote myself from the workaround PR linked above:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A real solution would be intercepting the call with an OkHttp network interceptor, but we can't pass in individual callbacks per different Retrofit upload calls with that approach (at least not easily).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the problem was that we had no way to get the &lt;code&gt;callback&lt;/code&gt; value from the call site that invokes the Retrofit method down to the interceptor attached to the underlying OkHttp client.&lt;/p&gt;

&lt;p&gt;Turns out, thankfully, that I was wrong about that!&lt;/p&gt;

&lt;h2&gt;
  
  
  OkHttp Tags
&lt;/h2&gt;

&lt;p&gt;Enter OkHttp's &lt;a href="https://square.github.io/okhttp/4.x/okhttp/okhttp3/-request/-builder/tag/"&gt;tags API&lt;/a&gt; (many thanks to &lt;a href="https://twitter.com/jessewilson"&gt;Jesse Wilson&lt;/a&gt; for showing me this!). With this API, the library allows you to add arbitrary tags (objects) to your requests when building them. &lt;/p&gt;

&lt;p&gt;As the &lt;a href="https://square.github.io/okhttp/4.x/okhttp/okhttp3/-request/-builder/tag/"&gt;docs&lt;/a&gt; say, you can:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use this API to attach timing, debugging, or other application data to a request so that you may read it in interceptors, event listeners, or callbacks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When building the &lt;code&gt;Request&lt;/code&gt;, you can pass in a &lt;code&gt;Class&lt;/code&gt; as a key and then an object of that type as the associated value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProgressCallback&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then later you can read these values from the &lt;code&gt;Request&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ProgressCallback&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProgressCallback&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One small problem is that in our file upload code we create a &lt;code&gt;RequestBody&lt;/code&gt; manually, but not the actual &lt;code&gt;Request&lt;/code&gt; object, as that's created under the hood by Retrofit. &lt;/p&gt;

&lt;p&gt;Thankfully, the tagging API is also exposed through Retrofit, so you can add tags to methods using the &lt;a href="https://square.github.io/retrofit/2.x/retrofit/retrofit2/http/Tag.html"&gt;&lt;code&gt;@Tag&lt;/code&gt; annotation&lt;/a&gt; on parameters. We'll use this in the next section!&lt;/p&gt;

&lt;h2&gt;
  
  
  The last-interceptor-swaparoo
&lt;/h2&gt;

&lt;p&gt;The strategy then is the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the &lt;code&gt;ProgressCallback&lt;/code&gt; instance as a tag when making the Retrofit call.&lt;/li&gt;
&lt;li&gt;Create an interceptor &lt;em&gt;at the end of the interceptor chain&lt;/em&gt; that will check each outgoing request and wrap its &lt;code&gt;RequestBody&lt;/code&gt; into a &lt;code&gt;ProgressRequestBody&lt;/code&gt; if the callback is present on it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;First, we'll update the Retrofit interface to accept a &lt;code&gt;progressCallback&lt;/code&gt; parameter that will be used as a tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Multipart&lt;/span&gt;
&lt;span class="nd"&gt;@POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/channels/{type}/{id}/file"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;sendFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nd"&gt;@Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;channelType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@Part&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MultipartBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@Tag&lt;/span&gt; &lt;span class="n"&gt;progressCallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ProgressCallback&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;RetrofitCall&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UploadFileResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we'll implement the interceptor that reads this tag and performs the wrapping of the &lt;code&gt;RequestBody&lt;/code&gt; if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProgressInterceptor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Interceptor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Interceptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chain&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;progressCallback&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProgressCallback&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;progressCallback&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;proceed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;wrapRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;progressCallback&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;proceed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;wrapRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;progressCallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ProgressCallback&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;// Assume that any request tagged with a ProgressCallback is a POST&lt;/span&gt;
            &lt;span class="c1"&gt;// request and has a non-null body            &lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProgressRequestBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;progressCallback&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we'll add this interceptor to our &lt;code&gt;OkHttpClient&lt;/code&gt;, making sure it's the last one added. &lt;/p&gt;

&lt;p&gt;We'll add it as a &lt;a href="https://square.github.io/okhttp/interceptors/#network-interceptors"&gt;network interceptor&lt;/a&gt;, as we want it to be as close to the actual upload as possible, and it doesn't need to be involved in requests that don't go out to the network.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;baseClientBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addNetworkInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProgressInterceptor&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way nothing that previous interceptors do with the request will interfere with the progress reporting, as they'll still have the original &lt;code&gt;RequestBody&lt;/code&gt; to work with, and the special progress-tracking wrapper is only added at the very last moment before it actually goes out to the network.&lt;/p&gt;

&lt;p&gt;If you want to see this last change in detail, check out &lt;a href="https://github.com/GetStream/stream-chat-android/pull/2386"&gt;the corresponding PR on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-Up
&lt;/h2&gt;

&lt;p&gt;So that's the implementation we ended up with for now! You can find all of this code in the &lt;a href="https://github.com/GetStream/stream-chat-android/"&gt;Chat Android SDK's GitHub repository&lt;/a&gt; if you want to look at it in a real project.&lt;/p&gt;

&lt;p&gt;There is one remaining issue with this progress tracking: Whenever the body gets written, it's only written into local buffers, and what we track is how fast we're writing to that buffer. This then still needs to make it over the network, which can take some time. &lt;/p&gt;

&lt;p&gt;So the upload progress tends to get to 100% relatively quickly and then the request will remain pending for a while as the network call completes. &lt;/p&gt;

&lt;p&gt;This is as close to the socket (so to say) as we can get with these APIs. For a bit more info and references, see &lt;a href="https://github.com/GetStream/stream-chat-android/pull/2342#discussion_r709021729"&gt;this GitHub discussion&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to learn more about how Okio (and OkHttp, Retrofit, and Moshi) work super efficiently with data, watch &lt;a href="https://www.youtube.com/watch?v=WvyScM_S88c"&gt;A Few Ok Libraries by Jake Wharton&lt;/a&gt;. For an introduction to Moshi, check out &lt;a href="https://zsmb.co/appearances/droidcon-london-2021/"&gt;Say Hi to Moshi&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>android</category>
      <category>networking</category>
      <category>okhttp</category>
      <category>retrofit</category>
    </item>
    <item>
      <title>All About Opt-In Annotations</title>
      <dc:creator>Márton Braun</dc:creator>
      <pubDate>Thu, 30 Sep 2021 16:32:34 +0000</pubDate>
      <link>https://dev.to/zsmb13/all-about-opt-in-annotations-5h6c</link>
      <guid>https://dev.to/zsmb13/all-about-opt-in-annotations-5h6c</guid>
      <description>&lt;p&gt;The &lt;a href="https://kotlinlang.org/docs/opt-in-requirements.html" rel="noopener noreferrer"&gt;opt-in mechanism in Kotlin&lt;/a&gt; allows you to mark APIs that should be used carefully -- or perhaps not at all. If you mark a declaration (a class, a function, a property, anything really) as &lt;em&gt;opt-in required&lt;/em&gt;, using it will produce a warning or error in the code, prompting the user to explicitly opt in to using it. This can guarantee that there has been a conscious decision made about using the API on the use site.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is &lt;em&gt;very&lt;/em&gt; similar to how you deprecate declarations to prevent people from using them. You can read more about deprecations in &lt;a href="https://zsmb.co/maintaining-compatibility-in-kotlin-libraries/#deprecation" rel="noopener noreferrer"&gt;Maintaining Compatibility in Kotlin Libraries&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Opt-in APIs are really useful if you're building libraries, but it also applies if you want to control the visibility and usages of APIs within any multi-module project. You also have to know how to interact with opt-in APIs correctly as a user.&lt;/p&gt;

&lt;p&gt;In this article, we'll look at everything you need to know about opt-in APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to create them&lt;/li&gt;
&lt;li&gt;How to use them&lt;/li&gt;
&lt;li&gt;Real life use cases and useful conventions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And we'll also touch on some auxiliary topics around opt-in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Java interop&lt;/li&gt;
&lt;li&gt;Android usage&lt;/li&gt;
&lt;li&gt;API verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We have a lot of ground to cover, so let's get started. Feel free to use the links above to jump around to different parts of this guide!&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating Opt-In APIs
&lt;/h2&gt;

&lt;p&gt;First, let's see how you can create APIs that will require their users to opt-in to using them. This is done in two steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creating an opt-in marker annotation&lt;/li&gt;
&lt;li&gt;Marking APIs as opt-in required&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Creating an Opt-in Marker Annotation
&lt;/h4&gt;

&lt;p&gt;To create an opt-in marker annotation, create a new &lt;code&gt;annotation class&lt;/code&gt; and annotate it with &lt;a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-requires-opt-in/" rel="noopener noreferrer"&gt;&lt;code&gt;RequiresOptIn&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example.lib.core&lt;/span&gt;

&lt;span class="nd"&gt;@RequiresOptIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RequiresOptIn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WARNING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"This is an experimental API. It may be changed or removed in the future."&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;annotation&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExperimentalLibraryApi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This annotation has very similar properties to the &lt;a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-deprecated/" rel="noopener noreferrer"&gt;&lt;code&gt;@Deprecated&lt;/code&gt;&lt;/a&gt; annotation. Let's review what they are.&lt;/p&gt;

&lt;p&gt;First, you can set a severity &lt;code&gt;level&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;WARNING&lt;/code&gt; will show a yellow warning on the call site and a warning during compilation. This is a "soft" warning, as it can be ignored and the code will still compile.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ERROR&lt;/code&gt; will cause a "hard" compilation error on the call site. Anyone using an API that has this opt-in level &lt;em&gt;must&lt;/em&gt; explicitly opt-in to using it, otherwise their code won't compile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also set a &lt;code&gt;message&lt;/code&gt; that describes what the given opt-in marker annotation indicates. For this example, we're creating an annotation to mark APIs that are still experimental in a library, but are expected to become stable in later versions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that marking experimental APIs is just one use case for opt-in annotations. See the Use Cases and Conventions below for other examples.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You'll notice that the usage of the &lt;code&gt;RequiresOptIn&lt;/code&gt; annotation itself also produces the warning. This is because the opt-in system of the Kotlin Standard Library itself is still experimental. You can track the development progress &lt;a href="https://youtrack.jetbrains.com/issue/KT-22956" rel="noopener noreferrer"&gt;on this YouTrack issue&lt;/a&gt; -- it's currently expected to become stable in Kotlin 1.7. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;While this system might not be officially stable, it's already being widely used across popular Kotlin libraries.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To make that warning go away, you'll have to &lt;em&gt;opt in to using Kotlin's opt-in system&lt;/em&gt;. You can do so by adding the following code to your module's build configuration, which adds an argument to the Kotlin compiler doing exactly that: opting in to opt-in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jetbrains&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;kotlin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;gradle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;KotlinCompile&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;kotlinOptions&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;freeCompilerArgs&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'-Xopt-in=kotlin.RequiresOptIn'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Marking APIs as Opt-in Required
&lt;/h4&gt;

&lt;p&gt;With the marker annotation created, you can now mark pieces of API as experimental! For example, you can annotate a function like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ExperimentalLibraryApi&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;someExperimentalApi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling this function will now produce a warning on the call site:&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fht9koh4wo9ww1bck28xw.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fht9koh4wo9ww1bck28xw.png" alt="Warning on the call site of the function"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also place these markers on larger scopes. For example, you can mark an entire class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ExperimentalLibraryApi&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this in place, any usage of the class' APIs (as well as the usage of the type itself) will produce warnings:&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgoyhvpk3wx0zrwydnaiu.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgoyhvpk3wx0zrwydnaiu.png" alt="Warnings on the call site of the class"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's see how you can handle these on the call site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Opt-In APIs
&lt;/h2&gt;

&lt;p&gt;There are several ways of making the warnings and errors produced by calling opt-in APIs go away. Let's see when you want to use each of them.&lt;/p&gt;

&lt;h4&gt;
  
  
  Propagating the Opt-in Requirement
&lt;/h4&gt;

&lt;p&gt;One way to use an opt-in API is to mark the code that's calling it with the same opt-in annotation, propagating the requirement further:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ExperimentalLibraryApi&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;anotherExperimentalApi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;someExperimentalApi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows you to use other APIs that are marked with the same annotation within the implementation of this function. However, anyone using &lt;em&gt;this&lt;/em&gt; function will now have to deal with the opt-in requirement as well.&lt;/p&gt;

&lt;p&gt;This is useful within your own code to make sure that you don't accidentally create stable APIs that rely on experimental APIs in their implementation. If something relies on an experimental declaration, it too should likely be experimental.&lt;/p&gt;

&lt;p&gt;However, you shouldn't do this in client code, as that'd just be putting off the issue. Instead, you want to actually opt in there. We'll look at this next.&lt;/p&gt;

&lt;h4&gt;
  
  
  Opting in an Entire Module
&lt;/h4&gt;

&lt;p&gt;If you want to enable all usages of a certain kind of opt-in API in a module, you can use the same &lt;code&gt;-Xopt-in&lt;/code&gt; compiler option that you used to opt in to using opt-in earlier, but provide the specific annotation as the value.&lt;/p&gt;

&lt;p&gt;This configuration allows all usages of our experimental APIs within its module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jetbrains&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;kotlin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;gradle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;KotlinCompile&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;kotlinOptions&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;freeCompilerArgs&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'-Xopt-in=com.example.lib.core.ExperimentalLibraryApi'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should use this with care, as enabling a certain opt-in API module-wide can lead to creating lots of usages of that API accidentally. Since there are no warnings in the code this way, you can easily miss these usages.&lt;/p&gt;

&lt;p&gt;On the other hand, this fits certain use cases really well, such as opting in to using internal APIs that are shared between different modules of the same library.&lt;/p&gt;

&lt;h4&gt;
  
  
  Opting in Locally
&lt;/h4&gt;

&lt;p&gt;A more precise approach is opting in locally. You can do this for each usage of opt-in APIs, using the &lt;a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-opt-in/" rel="noopener noreferrer"&gt;&lt;code&gt;OptIn&lt;/code&gt;&lt;/a&gt; annotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;clientCode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@OptIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;InternalMyLibraryApi&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;someExperimentalApi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll notice that the usage of the &lt;code&gt;OptIn&lt;/code&gt; annotation itself also produces the warning. This, again, is due to the system itself being experimental. Interestingly, you have to opt in to &lt;code&gt;kotlin.RequiresOptIn&lt;/code&gt; in this case as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jetbrains&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;kotlin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;gradle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;KotlinCompile&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;kotlinOptions&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;freeCompilerArgs&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'-Xopt-in=kotlin.RequiresOptIn'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the configuration above, you can now freely use &lt;code&gt;OptIn&lt;/code&gt; to suppress opt-in related warnings and errors. This comes with the benefit that each usage of opt-in APIs will be clearly visible in the code thanks to the &lt;code&gt;OptIn&lt;/code&gt; annotation being present on the use site.&lt;/p&gt;

&lt;p&gt;You can opt-in at the statement level, like seen above, but also in broader scopes, such as for an entire function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@OptIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;InternalMyLibraryApi&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;clientCode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;someExperimentalApi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;someExperimentalApi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or even an entire file, using an &lt;a href="https://kotlinlang.org/docs/annotations.html#annotation-use-site-targets" rel="noopener noreferrer"&gt;annotation use-site target&lt;/a&gt;, &lt;code&gt;@file:&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;OptIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;InternalMyLibraryApi&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;firstFunction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;someExperimentalApi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;secondFunction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;someExperimentalApi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let's expand beyond using these annotations to mark experimental APIs, and see what other scenarios they can be useful in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Cases and Conventions
&lt;/h2&gt;

&lt;p&gt;Some of the best examples of opt-in APIs are in first-party Kotlin libraries. A great example is the &lt;a href="https://github.com/Kotlin/kotlinx.coroutines" rel="noopener noreferrer"&gt;kotlinx.coroutines&lt;/a&gt; library, which uses four different opt-in marker annotations across its API. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The annotation naming conventions used in this library are also widely adopted in other libraries. You should consider aligning your own opt-in usages with this as well.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These opt-in annotations all follow the &lt;code&gt;[Prefix][LibraryName]Api&lt;/code&gt; pattern, and each prefix carries different semantics. Let's see what each of them mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-experimental-coroutines-api/" rel="noopener noreferrer"&gt;&lt;code&gt;ExperimentalCoroutinesApi&lt;/code&gt;&lt;/a&gt;: Perhaps the most frequently used convention, it marks APIs that are not stable yet. These might have known issues or ongoing design discussions that can change the API or its behaviour in the future.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-internal-coroutines-api/" rel="noopener noreferrer"&gt;&lt;code&gt;InternalCoroutinesApi&lt;/code&gt;&lt;/a&gt;: Marks APIs that are only exposed for cross-module usage within a single library group, and should not be relied on by end users. They should be considered &lt;code&gt;internal&lt;/code&gt; and can change or be removed at any time. See &lt;a href="https://zsmb.co/talks/mastering-api-visibility/" rel="noopener noreferrer"&gt;Mastering API Visibility in Kotlin&lt;/a&gt; for a detailed example of how to use these.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-obsolete-coroutines-api/" rel="noopener noreferrer"&gt;&lt;code&gt;ObsoleteCoroutinesApi&lt;/code&gt;&lt;/a&gt;: A more niche marker than previous ones, this marks APIs that should not be used anymore. However, they are not deprecated yet, as they don't have replacements ready that users could migrate to.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-delicate-coroutines-api/" rel="noopener noreferrer"&gt;&lt;code&gt;DelicateCoroutinesApi&lt;/code&gt;&lt;/a&gt;: Another niche convention, delicate APIs should only be used in very specific situations, and users should make sure they understand the implications of using such APIs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On Android, Google's own Jetpack libraries also make generous use of opt-in APIs. In the &lt;a href="https://android.googlesource.com/platform/frameworks/support/+/androidx-main/docs/api_guidelines.md#experimental-api" rel="noopener noreferrer"&gt;API guidelines document for Jetpack&lt;/a&gt;, using opt-in annotations is the recommended way of "annotating API surfaces as unstable".&lt;/p&gt;

&lt;p&gt;As a result, you'll see lots of these annotations when interacting with Jetpack libraries. They are especially widespread in Jetpack Compose libraries, given that the framework is still very new. Some common examples of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.android.com/reference/kotlin/androidx/compose/runtime/InternalComposeApi" rel="noopener noreferrer"&gt;&lt;code&gt;InternalComposeApi&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.android.com/reference/kotlin/androidx/compose/runtime/ExperimentalComposeApi" rel="noopener noreferrer"&gt;&lt;code&gt;ExperimentalComposeApi&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.android.com/reference/kotlin/androidx/compose/foundation/InternalFoundationApi" rel="noopener noreferrer"&gt;&lt;code&gt;InternalFoundationApi&lt;/code&gt;&lt;/a&gt; and&lt;a href="https://developer.android.com/reference/kotlin/androidx/compose/foundation/ExperimentalFoundationApi" rel="noopener noreferrer"&gt;&lt;code&gt;ExperimentalFoundationApi&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/reference/kotlin/androidx/compose/material/ExperimentalMaterialApi" rel="noopener noreferrer"&gt;&lt;code&gt;ExperimentalMaterialApi&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/text/InternalTextApi" rel="noopener noreferrer"&gt;&lt;code&gt;InternalTextApi&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/text/ExperimentalTextApi" rel="noopener noreferrer"&gt;&lt;code&gt;ExperimentalTextApi&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/InternalComposeUiApi" rel="noopener noreferrer"&gt;&lt;code&gt;InternalComposeUiApi&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/ExperimentalComposeUiApi" rel="noopener noreferrer"&gt;&lt;code&gt;ExperimentalComposeUiApi&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/test/InternalTestApi" rel="noopener noreferrer"&gt;&lt;code&gt;InternalTestApi&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/test/ExperimentalTestApi" rel="noopener noreferrer"&gt;&lt;code&gt;ExperimentalTestApi&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;... and more!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As you can see, these usually come in pairs, and a separate pair is created for each of the different Compose packages.&lt;/p&gt;

&lt;p&gt;They are generally not documented in Kdoc, but you can find out what their purpose is by reading the provided &lt;code&gt;message&lt;/code&gt;. A good place to do this is on &lt;a href="https://cs.android.com/" rel="noopener noreferrer"&gt;cs.android.com&lt;/a&gt;. For example, you can find the sources of &lt;code&gt;InternalFoundationApi&lt;/code&gt; &lt;a href="https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/InternalFoundationApi.kt" rel="noopener noreferrer"&gt;here&lt;/a&gt;, and the sources of &lt;code&gt;ExperimentalFoundationApi&lt;/code&gt; &lt;a href="https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ExperimentalFoundationApi.kt" rel="noopener noreferrer"&gt;here&lt;/a&gt;, in the same package.&lt;/p&gt;

&lt;p&gt;They, of course, follow the conventions layed out by &lt;em&gt;kotlinx.coroutines&lt;/em&gt; above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Java Interop
&lt;/h2&gt;

&lt;p&gt;Until now, we've discussed using Kotlin's own opt-in annotations: &lt;code&gt;kotlin.RequiresOptIn&lt;/code&gt; and &lt;code&gt;kotlin.OptIn&lt;/code&gt;. Usages of these are verified by the Kotlin compiler, which produces warnings and errors around them as needed.&lt;/p&gt;

&lt;p&gt;In most cases, the API marked with opt-in annotations is public, but the annotation being present deters clients from using it. However, when Java source code uses these APIs, they'll only see it as regular public API. The Java compiler does not understand or respect these opt-in requirements.&lt;/p&gt;

&lt;p&gt;You can either ignore this and accept that Java users (if they exist) will be able to access these APIs freely, and not be warned at all, or you can take extra steps to make sure that Java clients don't access opt-in APIs accidentally.&lt;/p&gt;

&lt;p&gt;One solution is to mark your opt-in declarations with &lt;a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-jvm-synthetic/" rel="noopener noreferrer"&gt;&lt;code&gt;@JvmSynthetic&lt;/code&gt;&lt;/a&gt; as well. Synthetic declarations are inaccessible from Java source code, preventing all Java usages of these APIs (with no way to opt-in).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@JvmSynthetic&lt;/span&gt;
&lt;span class="nd"&gt;@ExperimentalLibraryApi&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;someExperimentalApi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're on Android, keep reading for another one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Opt-In on Android
&lt;/h2&gt;

&lt;p&gt;Android is now officially &lt;a href="https://developer.android.com/kotlin/first" rel="noopener noreferrer"&gt;Kotlin-first&lt;/a&gt;, but Java is also still a fully-supported language on the platform. To work around the Java compatibility issue described above, Android has its own set of opt-in annotations (&lt;a href="https://developer.android.com/reference/kotlin/androidx/annotation/RequiresOptIn" rel="noopener noreferrer"&gt;&lt;code&gt;androidx.annotation.RequiresOptIn&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.android.com/reference/kotlin/androidx/annotation/OptIn" rel="noopener noreferrer"&gt;&lt;code&gt;androidx.annotation.OptIn&lt;/code&gt;&lt;/a&gt;), which can be used the exact same way as described earlier for the Kotlin annotations.&lt;/p&gt;

&lt;p&gt;The difference is in how these are verified. Instead of the Kotlin compiler performing the checks, verifying these Android-specific annotations is up to the &lt;a href="https://developer.android.com/studio/write/lint" rel="noopener noreferrer"&gt;Android Lint tool&lt;/a&gt;. As Lint checks are performed on the &lt;a href="https://plugins.jetbrains.com/docs/intellij/uast.html" rel="noopener noreferrer"&gt;UAST (Unified Abstract Syntax Tree)&lt;/a&gt;, these annotations are verified not only in Kotlin, but also in Java code. This means that the opt-in marker annotation, its usage, and the code using the opt-in APIs can all be in either language.&lt;/p&gt;

&lt;p&gt;To use these annotations in your project, you need to add a dependency on the &lt;a href="https://developer.android.com/jetpack/androidx/releases/annotation" rel="noopener noreferrer"&gt;&lt;code&gt;annotation-experimental&lt;/code&gt; artifact&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"androidx.annotation:annotation-experimental:1.1.0"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;   
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that you can still use the Kotlin opt-in system on Android, you'll just miss out on Java support.&lt;/p&gt;

&lt;h2&gt;
  
  
  API Verification
&lt;/h2&gt;

&lt;p&gt;If you mark some pieces of public API in your module as experimental or internal using Opt-In annotations, you will probably want to ignore them when checking for changes (especially breaking changes) in your public API.&lt;/p&gt;

&lt;p&gt;A common tool you can use for Kotlin projects is the &lt;a href="https://github.com/Kotlin/binary-compatibility-validator" rel="noopener noreferrer"&gt;Binary compatibility validator&lt;br&gt;
plugin&lt;/a&gt;. This plugin calls annotations that mark API that shouldn't be considered public "non-public markers".&lt;/p&gt;

&lt;p&gt;These can be configured in the plugin's configuration block by adding the fully qualified names of the marker annotations to &lt;code&gt;nonPublicMarkers&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;apiValidation&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**
     * Set of annotations that exclude API from being public.
     * Typically, it is all kinds of `@InternalApi` annotations 
     * that mark effectively private API that cannot be actually 
     * private for technical reasons.
     */&lt;/span&gt;
    &lt;span class="n"&gt;nonPublicMarkers&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"com.example.lib.core.ExperimentalLibraryApi"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"com.example.lib.core.InternalLibraryApi"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;APIs annotated with non-public markers will be excluded from the API dumps of the validator plugin (they won't show up in the generated &lt;code&gt;.api&lt;/code&gt; files).&lt;/p&gt;

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

&lt;p&gt;That's -- hopefully -- everything you'd ever want to know about opt-in annotations, and more. With this, you should now be ready to use these annotations in your own library or multi-module projects as well, whether you just need to interact with experimental APIs while building your apps or create your own opt-in APIs.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://twitter.com/stewemetal" rel="noopener noreferrer"&gt;István Juhos&lt;/a&gt;, &lt;a href="https://twitter.com/arnogiu" rel="noopener noreferrer"&gt;Arnaud Giuliani&lt;/a&gt;, &lt;a href="https://twitter.com/tkuenneth" rel="noopener noreferrer"&gt;Thomas Künneth&lt;/a&gt;, and &lt;a href="https://twitter.com/brwngrldev" rel="noopener noreferrer"&gt;Annyce Davis&lt;/a&gt; for taking an early look and reviewing this post :)&lt;/p&gt;

&lt;p&gt;Follow me &lt;a href="https://twitter.com/zsmb13" rel="noopener noreferrer"&gt;@zsmb13 on Twitter&lt;/a&gt; to keep in touch and get notified of similar posts!&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>api</category>
      <category>library</category>
    </item>
    <item>
      <title>A Bit of Gradle Housekeeping</title>
      <dc:creator>Márton Braun</dc:creator>
      <pubDate>Wed, 01 Sep 2021 20:38:52 +0000</pubDate>
      <link>https://dev.to/zsmb13/a-bit-of-gradle-housekeeping-240c</link>
      <guid>https://dev.to/zsmb13/a-bit-of-gradle-housekeeping-240c</guid>
      <description>&lt;p&gt;While cleaning is traditionally a spring activity, let me invite you to do it at the end of summer this time around.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Yes, I'm still using Groovy because I never found a compelling reason to upgrade to Kotlin DSL 🤷‍&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Something I'm quite enthusiastic about is having the simplest and most default Gradle configuration for an Android project that you can possibly have.&lt;/p&gt;

&lt;p&gt;Why? Because simple and boring configuration is great! The more configuration you have in these files, the more custom your setup, the harder it is to navigate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When something weird is happening in your project, having simpler project setup avoids a lot of potential headache when you need to figure out what's going on.&lt;/li&gt;
&lt;li&gt;When someone new joins the project or starts working with the project configuration files, a simpler setup will make their lives easier too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, we'll look at various bits of configuration that you might have in your build files that you don't actually need anymore, or can replace with something simpler.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build tools version
&lt;/h3&gt;

&lt;p&gt;I'll start with a classic that you should have removed four years ago when &lt;a href="https://developer.android.com/studio/releases/gradle-plugin#3-0-0"&gt;version 3.0.0&lt;/a&gt; of the Android Gradle Plugin was released. Despite that, it still lingers around in many projects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;android&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;buildToolsVersion&lt;/span&gt; &lt;span class="s2"&gt;"30.0.3"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This version number is set by default - feel free to simply remove it from your build config!&lt;/p&gt;

&lt;h3&gt;
  
  
  Java version settings
&lt;/h3&gt;

&lt;p&gt;Continuing with a more recent change, if you're on &lt;a href="https://developer.android.com/studio/releases/gradle-plugin#java-8-default"&gt;AGP 4.2&lt;/a&gt; or later, you no longer have to specify that you want to target version 8 of the Java language (which gives you desugaring for &lt;a href="https://developer.android.com/studio/write/java8-support#supported_features"&gt;various features&lt;/a&gt; like lambdas and method references).&lt;/p&gt;

&lt;p&gt;This means you can remove this config, if you have it in your build files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;android&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;compileOptions&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sourceCompatibility&lt;/span&gt; &lt;span class="n"&gt;JavaVersion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;VERSION_1_8&lt;/span&gt;
        &lt;span class="n"&gt;targetCompatibility&lt;/span&gt; &lt;span class="n"&gt;JavaVersion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;VERSION_1_8&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Kotlin JVM target settings
&lt;/h3&gt;

&lt;p&gt;On a very similar note, we used to have to specify the JVM version that the Kotlin compiler should target explicitly (as the default was 1.6). Since &lt;a href="https://kotlinlang.org/docs/whatsnew15.html#new-default-jvm-target-1-8"&gt;Kotlin 1.5&lt;/a&gt; however, the default value for this is &lt;code&gt;1.8&lt;/code&gt;, which means you can remove this config as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;android&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;kotlinOptions&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;jvmTarget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1.8'&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  *SdkVersion
&lt;/h3&gt;

&lt;p&gt;Something not to completely remove but to slightly simplify is the various SDK versions you specify in a project. Traditionally, you have probably used the following properties to set these values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;android&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;compileSdkVersion&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;

    &lt;span class="n"&gt;defaultConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;minSdkVersion&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;
        &lt;span class="n"&gt;targetSdkVersion&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, since AGP 4.1.0, these are deprecated and should be replaced with variants that no longer have the &lt;code&gt;Version&lt;/code&gt; postfix. So, for example, &lt;code&gt;minSdkVersion&lt;/code&gt; is now just &lt;code&gt;minSdk&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;android&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;compileSdk&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;

    &lt;span class="n"&gt;defaultConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;minSdk&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;
        &lt;span class="n"&gt;targetSdk&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The &lt;code&gt;src/main/kotlin&lt;/code&gt; source set
&lt;/h3&gt;

&lt;p&gt;Back to things you can remove, let's talk a bit of Kotlin. If you prefer using the &lt;code&gt;src/main/kotlin&lt;/code&gt; folder instead of the &lt;code&gt;src/main/java&lt;/code&gt; folder to store your source files (because you're just &lt;em&gt;that&lt;/em&gt;  enthusiastic about using the language), you probably have configuration in your build files that resembles something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;sourceSets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;java&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;srcDir&lt;/span&gt; &lt;span class="s2"&gt;"src/$it.name/kotlin"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This simply adds the folder in question as a source set where files will be used as compilation sources.&lt;/p&gt;

&lt;p&gt;The good news is that &lt;a href="https://stackoverflow.com/questions/41467453/renaming-java-directory-to-kotlin-in-android-studio"&gt;since version 7.0.0&lt;/a&gt; of the Android Gradle Plugin, you no longer need this. &lt;code&gt;kotlin&lt;/code&gt; source folders are just supported by default!&lt;/p&gt;

&lt;h3&gt;
  
  
  Kotlin standard library dependency
&lt;/h3&gt;

&lt;p&gt;Let's wrap it up with a final Kotlin tip. The classic Kotlin Android project setup includes this variable set in the top level &lt;code&gt;build.gradle&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;buildscript&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;kotlin_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1.5.30"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then a reference to it in the module level &lt;code&gt;build.gradle&lt;/code&gt; file, where the Standard Library is included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s2"&gt;"org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, &lt;a href="https://kotlinlang.org/docs/whatsnew14.html#dependency-on-the-standard-library-added-by-default"&gt;since Kotlin 1.4.0&lt;/a&gt;, including the Kotlin Gradle plugin automatically adds the Kotlin Standard Library as a runtime dependency to your modules (since you wouldn't want to use the language without its Standard Library anyway).&lt;/p&gt;

&lt;p&gt;It will include the &lt;em&gt;correct&lt;/em&gt; version based on the module's properties, matching the version of the plugin, the platform you're running on, and the JVM target you have configured.&lt;/p&gt;

&lt;p&gt;This means that you can remove the explicit dependency on the stdlib from your build files, and as a consequence, you can likely remove the &lt;code&gt;kotlin_version&lt;/code&gt; variable as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Thanks for participating in this quick late summer project cleaning initiative. I hope you got to create a quick and simple PR to make your project config a bit leaner. See you next time!&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>android</category>
      <category>gradle</category>
    </item>
    <item>
      <title>Keeping public API in check with the Kotlin binary validator plugin</title>
      <dc:creator>Márton Braun</dc:creator>
      <pubDate>Wed, 14 Jul 2021 07:50:56 +0000</pubDate>
      <link>https://dev.to/zsmb13/keeping-public-api-in-check-with-the-kotlin-binary-validator-plugin-2b6m</link>
      <guid>https://dev.to/zsmb13/keeping-public-api-in-check-with-the-kotlin-binary-validator-plugin-2b6m</guid>
      <description>&lt;p&gt;Within the &lt;a href="https://github.com/GetStream/stream-chat-android"&gt;Stream Chat Android SDK&lt;/a&gt; project, we use the &lt;a href="https://github.com/Kotlin/binary-compatibility-validator"&gt;Kotlin binary compatibility validator plugin&lt;/a&gt; to keep track of all the changes we make to our public API. This is a first-party plugin by JetBrains, though it's still experimental (it's an &lt;a href="https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub"&gt;incubator project&lt;/a&gt; by JetBrains on GitHub).&lt;/p&gt;

&lt;p&gt;In this article, you'll learn what the plugin does, how to set up and configure it, and how you can use it to make your library project better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core concept
&lt;/h2&gt;

&lt;p&gt;Let's start with a TL;DR of what the plugin does:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It generates &lt;code&gt;.api&lt;/code&gt; files that describe the public API for each module, and if you make changes to public API, you have to explicitly update the content of the &lt;code&gt;.api&lt;/code&gt; files (otherwise, checks will fail, alerting you of the accidental API change).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By doing this, it guarantees that developers on the project are always aware of the exact changes they're making to public API. Since the plugin works on binary API, it will catch incompatible changes that might not be obvious when looking at source code. For example, when writing Kotlin code, using data classes, default implementations, or companion objects might create binary API that you're usually unaware of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup and configuration
&lt;/h2&gt;

&lt;p&gt;The binary validator is available as a simple Gradle plugin, which makes setup really quick. Just add this code to your top level &lt;code&gt;build.gradle&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;buildscript&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;classpath&lt;/span&gt; &lt;span class="s1"&gt;'org.jetbrains.kotlinx:binary-compatibility-validator:0.6.0'&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="nl"&gt;plugin:&lt;/span&gt; &lt;span class="s1"&gt;'binary-compatibility-validator'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the plugin added, it's now time to configure it. You can do this inside the &lt;code&gt;apiValidation&lt;/code&gt; block. Here's &lt;a href="https://github.com/GetStream/stream-chat-android/blob/fd990322367c97eafc165995de1f065a9c89d9ec/build.gradle#L66-L82"&gt;the configuration we use in our SDK&lt;/a&gt; as an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;apiValidation&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ignoredPackages&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;
            &lt;span class="s1"&gt;'com/getstream/sdk/chat/databinding'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'io/getstream/chat/android/ui/databinding'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;ignoredProjects&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="c1"&gt;// 2&lt;/span&gt;
            &lt;span class="s1"&gt;'stream-chat-android-docs'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'stream-chat-android-sample'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'stream-chat-android-ui-components-sample'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'stream-chat-android-test'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;nonPublicMarkers&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="c1"&gt;// 3&lt;/span&gt;
            &lt;span class="s1"&gt;'io.getstream.chat.android.core.internal.InternalStreamChatApi'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's look at each of the options we're using:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ignoredPackages&lt;/code&gt; allows you to exclude some packages from validation. In our case, we don't want to keep track of the files generated by &lt;a href="https://developer.android.com/topic/libraries/view-binding"&gt;View Binding&lt;/a&gt;. You could also use &lt;code&gt;ignoredClasses&lt;/code&gt; to exclude individual classes by name.&lt;/li&gt;
&lt;li&gt;By default, the plugin will be enabled for all modules of a project. We use &lt;code&gt;ignoredProjects&lt;/code&gt; to exclude non-published modules, like documentation and sample apps.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;nonPublicMarkers&lt;/code&gt; entry allows you to specify any Kotlin &lt;a href="https://kotlinlang.org/docs/opt-in-requirements.html"&gt;Opt-in annotations&lt;/a&gt; that you're using on API that's not considered publicly available. To learn more about these, watch &lt;a href="https://zsmb.co/talks/mastering-api-visibility/"&gt;Mastering API Visibility in Kotlin&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;To (re)generate the &lt;code&gt;.api&lt;/code&gt; files, you'll have to run the &lt;code&gt;apiDump&lt;/code&gt; Gradle task. When you're doing this for the first time, you should review all the generated &lt;code&gt;.api&lt;/code&gt; files to make sure that everything in there is &lt;em&gt;really supposed to be&lt;/em&gt; public API, and then commit them to your repository.&lt;/p&gt;

&lt;p&gt;Here's a small sample of what you'll see in these files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public abstract interface class io/getstream/chat/android/ui/StyleTransformer {
    public abstract fun transform (Ljava/lang/Object;)Ljava/lang/Object;
}

public final class io/getstream/chat/android/ui/common/Debouncer {
    public fun &amp;lt;init&amp;gt; (J)V
    public final fun shutdown ()V
    public final fun submit (Lkotlin/jvm/functions/Function0;)V
    public final fun submitSuspendable (Lkotlin/jvm/functions/Function1;)V
}

public abstract interface class io/getstream/chat/android/ui/common/UrlSigner {
    public abstract fun signFileUrl (Ljava/lang/String;)Ljava/lang/String;
    public abstract fun signImageUrl (Ljava/lang/String;)Ljava/lang/String;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then use the &lt;code&gt;apiCheck&lt;/code&gt; Gradle task to verify that your current source code still has the same API as your committed &lt;code&gt;.api&lt;/code&gt; files. This task will create up-to-date &lt;code&gt;.api&lt;/code&gt; dumps in temporary build folders, and compare them to the &lt;code&gt;.api&lt;/code&gt; files you have in your repository. If they don't match, it will fail, and report the differences detected.&lt;/p&gt;

&lt;p&gt;For example, see this error the task generates after moving a property from a data class' constructor to its body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution failed for task ':stream-chat-android-client:apiCheck'.
&amp;gt; API check failed for project stream-chat-android-client.
   @@ -218,9 +218,8 @@

   public final class io/getstream/chat/android/client/api/models/AutocompleteFilterObject : io/getstream/chat/android/client/api/models/FilterObject {
    public final fun component1 ()Ljava/lang/String;
  - public final fun component2 ()Ljava/lang/String;
  - public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/chat/android/client/api/models/AutocompleteFilterObject;
  + public final fun copy (Ljava/lang/String;)Lio/getstream/chat/android/client/api/models/AutocompleteFilterObject;
    public fun equals (Ljava/lang/Object;)Z
    public final fun getFieldName ()Ljava/lang/String;
    public final fun getValue ()Ljava/lang/String;

   You can run :stream-chat-android-client:apiDump task to overwrite API declarations

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

&lt;/div&gt;



&lt;p&gt;If those differences are intentional, you should run the &lt;code&gt;apiDump&lt;/code&gt; task again, which updates the stored &lt;code&gt;.api&lt;/code&gt; files, now including any changes in the API you've created by changing the source code. Running the &lt;code&gt;apiCheck&lt;/code&gt; task at this point will complete successfully, as the sources and the &lt;code&gt;.api&lt;/code&gt; files are in sync again.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the goal of the plugin: making developers explicitly run a task to update the API description files when they touch public API.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You should then commit the changes you've introduced to the &lt;code&gt;.api&lt;/code&gt; files. These will show up in commits and on pull requests, allowing reviewers to easily tell when you've made changes to the binary API, and catch any unintentional changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Git hooks and CI checks
&lt;/h3&gt;

&lt;p&gt;The power of the plugin really shows if you have &lt;strong&gt;automated checks&lt;/strong&gt; in place to make sure that whenever you change your public API, you've actually updated the &lt;code&gt;.api&lt;/code&gt; files to match.&lt;/p&gt;

&lt;p&gt;At Stream, we run the checks in two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using a pre-commit &lt;a href="https://github.com/GetStream/stream-chat-android/blob/fd990322367c97eafc165995de1f065a9c89d9ec/hooks/pre-commit#L16-L25"&gt;git hook&lt;/a&gt; in our repository that will run the &lt;code&gt;apiCheck&lt;/code&gt; task (and automatically run &lt;code&gt;apiDump&lt;/code&gt; for you if it fails) when you try to create a new commit in the repository.&lt;/li&gt;
&lt;li&gt;With &lt;a href="https://github.com/GetStream/stream-chat-android/blob/fd990322367c97eafc165995de1f065a9c89d9ec/.github/workflows/pr-checks.yml#L19-L30"&gt;a GitHub Action step&lt;/a&gt; in our PR checks workflow that makes sure that the &lt;code&gt;api&lt;/code&gt; files are up-to-date before we merge changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;This plugin is a &lt;em&gt;binary&lt;/em&gt; validator. This means that it doesn't track public API changes that don't affect binary compatibility. &lt;a href="https://github.com/Kotlin/binary-compatibility-validator/issues/35"&gt;For example, reified functions are part of source-level API but are not present in the binary API&lt;/a&gt;. Thankfully, changes in these should be obvious from the source file changes anyway (and they're not very frequent).&lt;/p&gt;

&lt;p&gt;There are also some known issues in the library, &lt;a href="https://github.com/Kotlin/binary-compatibility-validator/issues"&gt;as you can see on GitHub&lt;/a&gt;. We've reported some of these ourselves, mostly for problems in respecting non-public markers, which we use extensively in our project (see &lt;a href="https://github.com/Kotlin/binary-compatibility-validator/issues/36"&gt;#36&lt;/a&gt; and &lt;a href="https://github.com/Kotlin/binary-compatibility-validator/issues/58"&gt;#58&lt;/a&gt;).&lt;/p&gt;

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

&lt;p&gt;Even with those limitations in mind, the binary compatibility validator plugin is a great tool for making sure that you don't make any accidental changes in your public binary API when building a library. Check it out &lt;a href="https://github.com/Kotlin/binary-compatibility-validator"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You'll also find our &lt;a href="https://github.com/GetStream/stream-chat-android"&gt;Android Chat SDK on GitHub&lt;/a&gt;, and you can try our &lt;a href="https://getstream.io/tutorials/android-chat/"&gt;In-App Messaging Tutorial&lt;/a&gt; to get started with it.&lt;/p&gt;

&lt;p&gt;More library development topics you might be interested in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Our guide to &lt;a href="https://getstream.io/blog/publishing-libraries-to-mavencentral-2021/"&gt;Publishing Android libraries to MavenCentral&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zsmb.co/maintaining-compatibility-in-kotlin-libraries/"&gt;Maintaining Compatibility in Kotlin Libraries
&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zsmb.co/mastering-api-visibility-in-kotlin/"&gt;Mastering API Visibility in Kotlin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>library</category>
    </item>
    <item>
      <title>Publishing Android libraries to MavenCentral in 2021</title>
      <dc:creator>Márton Braun</dc:creator>
      <pubDate>Wed, 21 Apr 2021 14:29:27 +0000</pubDate>
      <link>https://dev.to/zsmb13/publishing-android-libraries-to-mavencentral-in-2021-44oh</link>
      <guid>https://dev.to/zsmb13/publishing-android-libraries-to-mavencentral-in-2021-44oh</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;This is an updated version of an article published two years ago on &lt;a href="https://blog.autsoft.hu/publishing-an-android-library-to-mavencentral-in-2019/"&gt;blog.autsoft.hu&lt;/a&gt;. This new version supports non-Android libraries, uses command line GPG handling instead of a specific Windows GUI app, includes new best practices, and uses GitHub Actions for its CI integration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Update, April 2021: addressed &lt;a href="https://central.sonatype.org/news/20210223_new-users-on-s01/"&gt;new Sonatype infra&lt;/a&gt;, moved to using &lt;a href="https://developer.android.com/studio/build/maven-publish-plugin"&gt;AGP Maven Publish plugin integration&lt;/a&gt; for release components, and to using the new &lt;a href="https://github.com/gradle-nexus/publish-plugin/"&gt;gradle-nexus/publish-plugin&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update, July 2021: fixed keyserver being used (&lt;a href="https://twitter.com/Ch8n2/status/1412782176702058496"&gt;thanks Chetan Gupta&lt;/a&gt;), moved to using in-memory GPG keys for signing (&lt;a href="https://twitter.com/arkann1985/status/1412751173698965510"&gt;thanks Arkadii Ivanov&lt;/a&gt;).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update, July 2021: use JDK11 in GitHub Actions configuration to support new AGP versions.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Creating a great library is hard work. Coming up with the idea, implementing it, making sure you have a nice, stable public API that you &lt;a href="https://zsmb.co/mastering-api-visibility-in-kotlin/"&gt;control carefully&lt;/a&gt; and &lt;a href="https://zsmb.co/maintaining-compatibility-in-kotlin-libraries/"&gt;maintain&lt;/a&gt;… That’s already lots to do.&lt;/p&gt;

&lt;p&gt;After all that, you need to make your library available to the public. Technically, you could distribute the &lt;code&gt;.aar&lt;/code&gt; file any way you want, but the norm is publishing it to a publicly available Maven repository. It’s a good idea to use one of the well-established repositories that people are already likely to have in their projects, to make getting started with your library as easy as possible.&lt;/p&gt;

&lt;p&gt;The simplest choice would be &lt;strong&gt;JitPack&lt;/strong&gt;, which might not give you much in terms of customization or control, but is very easy to &lt;a href="https://jitpack.io/"&gt;get started&lt;/a&gt; with. All you have to do is publish your project on GitHub, and JitPack should be able to build and distribute it immediately. If you’re new to libraries, this is a great choice for getting your code out there.&lt;/p&gt;

&lt;p&gt;The next step up is &lt;strong&gt;Jcenter&lt;/strong&gt;… &lt;a href="https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/"&gt;Which no longer accepts new packages and will stop serving them in February 2022&lt;/a&gt;. Using it &lt;a href="https://blog.autsoft.hu/a-confusing-dependency/"&gt;had its issues&lt;/a&gt; anyway.&lt;/p&gt;

&lt;p&gt;Finally, the fanciest place you can be in is The Central Repository via Sonatype OSSRH (OSS Repository Hosting), which I’ll refer to as simply &lt;strong&gt;MavenCentral&lt;/strong&gt; from here on. This is &lt;em&gt;the&lt;/em&gt; place to be if you’re a Maven dependency. Artifacts on MavenCentral are well trusted, and their integrity can be verified, as they are all required to be signed by the author.&lt;/p&gt;

&lt;p&gt;The publication process, however, and especially automating it, can be quite a headache. It’s easy to get stuck at many of the various steps no matter what tutorials you’re following, especially if they’re out of date, and this can get demotivating very quickly. It’s not uncommon to &lt;a href="https://issues.sonatype.org/browse/OSSRH-38347?focusedCommentId=505686&amp;amp;page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-505686"&gt;give up and just use Bintray/Jcenter instead&lt;/a&gt; – which is not an option anymore.&lt;/p&gt;

&lt;p&gt;So, if you feel up for a bit of a challenge, and want to do things the right way, here’s how you can get a library into MavenCentral, in 2021.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;Here's a quick overview of the steps we'll go through:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Registering a Jira account with Sonatype, and verifying your ownership of the group ID you want to publish your artifact with&lt;/li&gt;
&lt;li&gt;Generating a GPG key pair for signing your artifacts, publishing your public key, and exporting your private key&lt;/li&gt;
&lt;li&gt;Setting up Gradle tasks that can sign upload your artifacts to a staging repository&lt;/li&gt;
&lt;li&gt;Manually going through the process of checking your artifacts in the staging repo and releasing them via the Sonatype web UI&lt;/li&gt;
&lt;li&gt;Automating the close &amp;amp; release flow with a Gradle plugin&lt;/li&gt;
&lt;li&gt;Configuring CI workflows with GitHub Actions to automate all of the above&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A lot of ground to cover - let’s go!&lt;/p&gt;

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

&lt;p&gt;We’ll be using the following tools for this tutorial. You are free to use alternatives, but these are our favourites, and they work well for us.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The command line &lt;code&gt;gpg&lt;/code&gt; tool

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://gpgtools.org/"&gt;GPG Suite&lt;/a&gt; for macOS or &lt;a href="https://www.gpg4win.org/download.html"&gt;Gpg4win&lt;/a&gt; for Windows are great choices, which also come with helpful GUIs. Many alternatives for different platforms are available &lt;a href="https://gnupg.org/download/"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;GitHub as the public host of the library’s repository&lt;/li&gt;
&lt;li&gt;GitHub Actions as the continuous integration solution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this article, we’ll assume that you already have your library developed, and have uploaded it to a public GitHub repository.&lt;/p&gt;

&lt;p&gt;We’ll use &lt;a href="https://github.com/GetStream/stream-chat-android"&gt;our very own Android Chat SDK&lt;/a&gt; in our examples. This &lt;a href="https://getstream.io/chat/sdk/android/"&gt;SDK&lt;/a&gt; is made up of multiple artifacts, but for simplification, we’ll just talk about publishing the low-level networking client, which lives in the &lt;code&gt;stream-chat-android-client&lt;/code&gt; module of the GitHub repository.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Shameless plug: if you need a chat solution in your Android app, check out &lt;a href="https://getstream.io/chat/docs/android/?language=kotlin"&gt;our documentation&lt;/a&gt; and our &lt;a href="https://getstream.io/tutorials/android-chat/"&gt;Android Chat tutorial&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Registering a Sonatype account
&lt;/h2&gt;

&lt;p&gt;First things first, you’ll need an account in the &lt;a href="https://issues.sonatype.org/secure/Dashboard.jspa"&gt;Sonatype Jira&lt;/a&gt;. Head over there and hit &lt;em&gt;Sign up&lt;/em&gt;. Registration is straightforward, it just requires a username, an email, and a password.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aFzDV2pb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iuri8a9vlo2rmc6ls4p6.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aFzDV2pb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iuri8a9vlo2rmc6ls4p6.PNG" alt="The login page to Sonatype Jira" width="482" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After you’ve logged in, you’ll need to open an issue, asking for access to the group ID that you’ll want to publish your project under. For us, based on our domain name (&lt;code&gt;getstream.io&lt;/code&gt;), our group ID is &lt;code&gt;io.getstream&lt;/code&gt;. If you own a domain, it’s best to choose the reversed version of that as your group ID. Otherwise, you’ll have to stick with having a GitHub-based group ID (see &lt;a href="https://central.sonatype.org/pages/choosing-your-coordinates.html"&gt;Choosing your Coordinates&lt;/a&gt; for more details).&lt;/p&gt;

&lt;p&gt;After choosing a language and an avatar, you’ll end up on this landing page - click on &lt;em&gt;Create an issue&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GE_o5D75--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/50y3gntbi545vp3k790j.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GE_o5D75--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/50y3gntbi545vp3k790j.PNG" alt="Sonatype Jira landing page" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select &lt;em&gt;Community Support - Open Source Project Repository Hosting&lt;/em&gt; and then &lt;em&gt;New Project&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gsEYMLb5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rncqgg6bqrmd1rowdkzo.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gsEYMLb5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rncqgg6bqrmd1rowdkzo.PNG" alt="Creating a new issue in Jira" width="800" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the next page, fill out the following fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Summary:&lt;/strong&gt; &lt;em&gt;Create repository for your.group.id.here&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Description:&lt;/strong&gt; An optional, quick summary of what your project is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group Id:&lt;/strong&gt; Your group ID, as described a few sections earlier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project URL:&lt;/strong&gt; If your project has a webpage, the URL of that page. This can also be just the GitHub repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SCM url:&lt;/strong&gt; Your source control URL, i.e. the GitHub repository link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Username(s):&lt;/strong&gt; If you want additional users (on top of the one you’re using for this process) to have deploy access for your group ID, you can list them here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Already Synced to Central:&lt;/strong&gt; If you’re just getting started, this should be &lt;em&gt;No&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--G39N_DRP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5r4kqjm14gc0f77h9m5i.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--G39N_DRP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5r4kqjm14gc0f77h9m5i.PNG" alt="The filled out issue details page for a new repository" width="800" height="1023"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Soon after opening it, your issue will get a comment telling you to verify that you own the domain corresponding to your group ID:&lt;/p&gt;

&lt;p&gt;![Comment requesting domain verification](&lt;a href="https://stream-blog-v2.imgix.net/blog/wp-content/uploads/da8ea3aa9e7861342114d89a78f7e7ef/please_verify_2-0-00-00-00.png?auto=compress%2Cformat&amp;amp;ixlib=php-3.3.0%5D(https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hgsfecmkirg0f8bmf18g.PNG)"&gt;https://stream-blog-v2.imgix.net/blog/wp-content/uploads/da8ea3aa9e7861342114d89a78f7e7ef/please_verify_2-0-00-00-00.png?auto=compress%2Cformat&amp;amp;ixlib=php-3.3.0](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hgsfecmkirg0f8bmf18g.PNG)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To comply with this, add the required TXT record to your domain - how to do this will depend on where your domain is registered, but it should be a fairly simple task.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@    TXT    1800    OSSRH-12345
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Make sure to replace the ticket number above with your actual number.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When done, don’t forget to leave a comment on the issue so that Sonatype knows to check the record. You’ll eventually get a response telling you that you now have deploy rights - congrats!&lt;/p&gt;

&lt;p&gt;![The Sonatype comment confirming domain verification]](&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/niwgnp6n317gkapz14cb.PNG"&gt;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/niwgnp6n317gkapz14cb.PNG&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating a GPG key pair
&lt;/h2&gt;

&lt;p&gt;As we eluded to earlier, artifacts published on MavenCentral have to be signed by their publishers. You’ll need a GPG key for this.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;MavenCentral also has its own documentation for &lt;a href="https://central.sonatype.org/pages/working-with-pgp-signatures.html"&gt;Working with PGP Signatures&lt;/a&gt; which you can reference if you get stuck along the way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This part requires access to the &lt;code&gt;gpg&lt;/code&gt; command. There are several ways to install this via package managers, and there are many distributions available for different platforms on &lt;a href="https://gnupg.org/download/"&gt;gnupg.org&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gpgtools.org/"&gt;GPG Suite&lt;/a&gt; is an easy-to-use distribution for macOS, and you can use &lt;a href="https://www.gpg4win.org/download.html"&gt;Gpg4win&lt;/a&gt; if you’re on Windows. Both of these come with GUI tools that make managing keys easier.&lt;/p&gt;

&lt;p&gt;We'll stick to the command line here, but note that the &lt;a href="https://blog.autsoft.hu/publishing-an-android-library-to-mavencentral-in-2019/#creatingagpgkeypair"&gt;old article&lt;/a&gt; shows how to use the Kleopatra GUI (included in Gpg4win) on Windows for generating and managing keys.&lt;/p&gt;

&lt;p&gt;To generate a new key, run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gpg --full-gen-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll be prompted to enter a few details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kind of key:&lt;/strong&gt; Accept the default value, which is &lt;code&gt;(1) RSA and RSA&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key size:&lt;/strong&gt; &lt;code&gt;4096&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiration:&lt;/strong&gt; You can input &lt;code&gt;0&lt;/code&gt; to generate a key that never expires. You can also create a key that has an expiry date and then &lt;a href="https://central.sonatype.org/pages/working-with-pgp-signatures.html#dealing-with-expired-keys"&gt;renew it&lt;/a&gt; periodically, if you prefer to do so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real name, email:&lt;/strong&gt; Should be obvious.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comment:&lt;/strong&gt; Freeform text, can be left empty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After entering these details, you’ll be prompted to enter a passphrase to secure your key with.&lt;/p&gt;

&lt;p&gt;Here’s the full flow you'll go through, with a bit of truncation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Please select what kind of key you want:
   (1) RSA and RSA (default)
   ...
Your selection? 1

RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (3072) 4096
Requested keysize is 4096 bits

Please specify how long the key should be valid.
         0 = key does not expire
         ...
Key is valid for? (0) 0
Key does not expire at all
Is this correct? (y/N) y

GnuPG needs to construct a user ID to identify your key.

Real name: Marton Braun
Email address: marton@getstream.io
Comment: Example key for tutorial
You selected this USER-ID:
    "Marton Braun (Example key for tutorial) &amp;lt;marton@getstream.io&amp;gt;"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O

gpg: key 36271B955BEF072A marked as ultimately trusted
gpg: revocation certificate stored as '.../gnupg/openpgp-revocs.d\7A5D73CFEDDDBC915986998A36271B955BEF072A.rev'
public and secret key created and signed.

pub   rsa4096 2021-02-03 [SC]
      7A5D73CFEDDDBC915986998A36271B955BEF072A
uid                      Marton Braun (Example key for tutorial) &amp;lt;marton@getstream.io&amp;gt;
sub   rsa4096 2021-02-03 [E]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can always check the keys you have on your system by running &lt;code&gt;gpg --list-keys&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gpg: checking the trustdb
.../gnupg/pubring.kbx
-----------------------------------------------
pub   rsa4096 2021-02-03 [SC]
      7A5D73CFEDDDBC915986998A36271B955BEF072A
uid           [ultimate] Marton Braun (Example key for tutorial) &amp;lt;marton@getstream.io&amp;gt;
sub   rsa4096 2021-02-03 [E]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your key’s ID is the last eight digits of its fingerprint (the long hexadecimal string above). In this case, this is &lt;code&gt;5BEF072A&lt;/code&gt; - take note of this, as you’ll use this later.&lt;/p&gt;

&lt;p&gt;You’ve generated a pair of keys - a &lt;em&gt;private&lt;/em&gt; and a &lt;em&gt;public&lt;/em&gt; key. You’ll keep the &lt;em&gt;private&lt;/em&gt; one hidden and use it to sign your artifacts. The public key has to be uploaded to the server so that anyone can check that it belongs to you, which you can do by running the following (&lt;em&gt;use your own key ID!&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gpg --keyserver keyserver.ubuntu.com --send-keys 5BEF072A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;See &lt;a href="https://central.sonatype.org/publish/requirements/gpg/#distributing-your-public-key"&gt;Distributing Your Public Key&lt;/a&gt; for the list of supported keyservers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your private key will need to be referenced by your project when it signs the artifacts. You can get a base 64 export of it with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gpg --export-secret-keys 5BEF072A | base64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter your passphrase that you’ve set earlier when prompted. You can either copy-paste this key from the terminal output later on, or save it to a file temporarily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up publication in your project
&lt;/h2&gt;

&lt;p&gt;That’s a lot of work without touching your project, but the time has come to do that now. In the next few steps, you will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add Gradle scripts that set up the publication plugin required to push artifacts to a repository.&lt;/li&gt;
&lt;li&gt;Configure the properties of the library you’re releasing.&lt;/li&gt;
&lt;li&gt;Grab the necessary authentication details along with the private key you’ve just exported.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Root project Gradle configuration
&lt;/h3&gt;

&lt;p&gt;To easily automate publishing later, you'll use the &lt;a href="https://github.com/gradle-nexus/publish-plugin/"&gt;gradle-nexus/publish-plugin&lt;/a&gt; tool. This has to be added in your project level (root) &lt;code&gt;build.gradle&lt;/code&gt; file as a dependency.&lt;/p&gt;

&lt;p&gt;You can do this &lt;em&gt;either&lt;/em&gt; with a &lt;code&gt;plugins&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"io.github.gradle-nexus.publish-plugin"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="s2"&gt;"1.1.0"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Or&lt;/em&gt; with the classic buildscript dependency syntax, and then applying it in your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;buildscript&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;classpath&lt;/span&gt; &lt;span class="s1"&gt;'io.github.gradle-nexus:publish-plugin:1.1.0'&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="nl"&gt;plugin:&lt;/span&gt; &lt;span class="s1"&gt;'io.github.gradle-nexus.publish-plugin'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Check the &lt;a href="https://github.com/gradle-nexus/publish-plugin/releases"&gt;releases page&lt;/a&gt; of the plugin to get the latest version.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next, create a new file called &lt;code&gt;publish-root.gradle&lt;/code&gt; in a new &lt;code&gt;scripts&lt;/code&gt; folder inside your project. This will contain global configuration you need for publishing, grabbing input values for your scripts, and defining the MavenCentral repository.&lt;/p&gt;

&lt;p&gt;Note that here we’ll go through the contents of these scripts part by part, with explanations. &lt;strong&gt;You can always find the complete, up-to-date files we use in production &lt;a href="https://github.com/GetStream/stream-chat-android/blob/develop/scripts/"&gt;in the Stream Chat repository&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this file, you'll grab some configuration parameters. Using the script below, you’ll first set all the variables to a dummy empty string. This will let the project sync and build without the publication values set up, which would otherwise be an issue for your contributors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create variables with empty default values&lt;/span&gt;
&lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"signing.keyId"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
&lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"signing.password"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
&lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"signing.key"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
&lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ossrhUsername"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
&lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ossrhPassword"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
&lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sonatypeStagingProfileId"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first three variables will be used to sign the artifacts after they’re built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;signing.keyId&lt;/code&gt;: the ID of the GPG key pair, the last eight characters of its fingerprint&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;signing.password&lt;/code&gt;: the passphrase of the key pair&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;signing.key&lt;/code&gt;: the base64 encoded private key you exported earlier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;osshrUsername&lt;/code&gt; and &lt;code&gt;ossrhPassword&lt;/code&gt;: are your account details for MavenCentral, which you’ve chosen at the Jira registration step. We’ll get back to where &lt;code&gt;sonatypeStagingProfileId&lt;/code&gt; comes from later on.&lt;/p&gt;

&lt;p&gt;Next, you’ll try to fetch the values of the variables from a &lt;code&gt;local.properties&lt;/code&gt; file in the root of the project if it exists, otherwise you’ll look for them in the environment variables. The former lets you easily input these values locally on your machine, while the latter will help with setting up CI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="n"&gt;secretPropsFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rootProject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'local.properties'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secretPropsFile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Read local.properties file first if it exists&lt;/span&gt;
    &lt;span class="n"&gt;Properties&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FileInputStream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secretPropsFile&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;withCloseable&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;each&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Use system environment variables&lt;/span&gt;
    &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ossrhUsername"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'OSSRH_USERNAME'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ossrhPassword"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'OSSRH_PASSWORD'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sonatypeStagingProfileId"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SONATYPE_STAGING_PROFILE_ID'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"signing.keyId"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SIGNING_KEY_ID'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"signing.password"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SIGNING_PASSWORD'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"signing.key"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SIGNING_KEY'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure that you’ve set these variables either in the aforementioned &lt;code&gt;local.properties&lt;/code&gt; file or in your environment variables. If you want to use the property file, the syntax for it should look something like this (&lt;em&gt;replace all the data here with your own!&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;signing.keyId=5BEF072A
signing.password=signingPass123
signing.key=bm90IGEgcmVhbCBrZXksIHdoYXQgd2VyZSB5b3UgZXhwZWN0aW5n
ossrhUsername=yourSonatypeUser
ossrhPassword=yourSonatypePassword
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last piece of code to add to this file will define the MavenCentral (Sonatype) repository where publishing should upload the artifacts. This relies on parameters fetched above to authenticate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Set up Sonatype repository&lt;/span&gt;
&lt;span class="n"&gt;nexusPublishing&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;repositories&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sonatype&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;stagingProfileId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sonatypeStagingProfileId&lt;/span&gt;
            &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ossrhUsername&lt;/span&gt;
            &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ossrhPassword&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; If you're on the new Sonatype infrastructure (happens if you've registered after 2021-02-24 or requested it specifically), you have to add explicit URLs pointing to &lt;code&gt;s01.oss.sonatype.org&lt;/code&gt; in this config block next to the existing parameters, like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;nexusPublishing&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;repositories&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sonatype&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="cm"&gt;/* Existing params here... */&lt;/span&gt;
            &lt;span class="n"&gt;nexusUrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://s01.oss.sonatype.org/service/local/"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;snapshotRepositoryUrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://s01.oss.sonatype.org/content/repositories/snapshots/"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the last step, apply this script in your root &lt;code&gt;build.gradle&lt;/code&gt; file like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="nl"&gt;from:&lt;/span&gt; &lt;span class="s2"&gt;"${rootDir}/scripts/publish-root.gradle"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This completes the configuration needed in the root project. &lt;a href="https://gist.github.com/zsmb13/56ed98c8fe916de441f2a9d8e060cd4a"&gt;This gist&lt;/a&gt; contains the code blocks detailed above in a single snippet. The final, up-to-date script we use for publishing our SDK can be found &lt;strong&gt;&lt;a href="https://github.com/GetStream/stream-chat-android/blob/develop/scripts/publish-root.gradle"&gt;in the Stream Chat repository&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-module Gradle setup
&lt;/h3&gt;

&lt;p&gt;With the repository defined and parameters fetched, it's time to create a script that will define the artifacts in each module you want to publish. Create a new file called &lt;code&gt;publish-module.gradle&lt;/code&gt; in the &lt;code&gt;scripts&lt;/code&gt; folder. As its name suggests, this will be applied to each &lt;em&gt;module&lt;/em&gt; that you want to publish an artifact from.&lt;/p&gt;

&lt;p&gt;In this file, first you’ll apply two plugins for publishing and signing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="nl"&gt;plugin:&lt;/span&gt; &lt;span class="s1"&gt;'maven-publish'&lt;/span&gt;
&lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="nl"&gt;plugin:&lt;/span&gt; &lt;span class="s1"&gt;'signing'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Our publishing file also applies the &lt;a href="https://github.com/Kotlin/dokka"&gt;Dokka plugin&lt;/a&gt; in this block, which you have to configure separately, if you want to use it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then you declare the sources artifact for the library. This is really important - it will make sure that the source files are packaged along with the executable, compiled code, so that your users can easily jump to the definitions that they’re calling into within their IDE.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="nf"&gt;androidSourcesJar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;type:&lt;/span&gt; &lt;span class="n"&gt;Jar&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;archiveClassifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sources'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findPlugin&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"com.android.library"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// For Android libraries&lt;/span&gt;
        &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;android&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sourceSets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;java&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;srcDirs&lt;/span&gt;
        &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;android&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sourceSets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;kotlin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;srcDirs&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// For pure Kotlin libraries, in case you have them&lt;/span&gt;
        &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sourceSets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;java&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;srcDirs&lt;/span&gt;
        &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sourceSets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;kotlin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;srcDirs&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;artifacts&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;archives&lt;/span&gt; &lt;span class="n"&gt;androidSourcesJar&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Once again, our real publishing file also adds an additional &lt;code&gt;javadocJar&lt;/code&gt; task which ships Dokka-generated documentation - again, you can skip this if you want to.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You’ll set two properties on the Gradle project itself here, the group ID and the version of the artifact. You’ll see where these uppercase values come from later on, when you apply this publication script in the module level &lt;code&gt;build.gradle&lt;/code&gt; files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PUBLISH_GROUP_ID&lt;/span&gt;
&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PUBLISH_VERSION&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here comes the complicated part, providing all the metadata for the library you’re releasing. See the inline comments for the play-by-play explanation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;afterEvaluate&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;publishing&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;publications&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MavenPublication&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// The coordinates of the library, being set from variables that&lt;/span&gt;
                &lt;span class="c1"&gt;// we'll set up later&lt;/span&gt;
                &lt;span class="n"&gt;groupId&lt;/span&gt; &lt;span class="n"&gt;PUBLISH_GROUP_ID&lt;/span&gt;
                &lt;span class="n"&gt;artifactId&lt;/span&gt; &lt;span class="n"&gt;PUBLISH_ARTIFACT_ID&lt;/span&gt;
                &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="n"&gt;PUBLISH_VERSION&lt;/span&gt;

                &lt;span class="c1"&gt;// Two artifacts, the `aar` (or `jar`) and the sources&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findPlugin&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"com.android.library"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;release&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;java&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;

                &lt;span class="n"&gt;artifact&lt;/span&gt; &lt;span class="n"&gt;androidSourcesJar&lt;/span&gt;
                &lt;span class="n"&gt;artifact&lt;/span&gt; &lt;span class="n"&gt;javadocJar&lt;/span&gt;

                &lt;span class="c1"&gt;// Mostly self-explanatory metadata&lt;/span&gt;
                &lt;span class="n"&gt;pom&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PUBLISH_ARTIFACT_ID&lt;/span&gt;
                    &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Stream Chat official Android SDK'&lt;/span&gt;
                    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://github.com/getstream/stream-chat-android'&lt;/span&gt;
                    &lt;span class="n"&gt;licenses&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;license&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                            &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Stream License'&lt;/span&gt;
                            &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://github.com/GetStream/stream-chat-android/blob/main/LICENSE'&lt;/span&gt;
                        &lt;span class="o"&gt;}&lt;/span&gt;
                    &lt;span class="o"&gt;}&lt;/span&gt;
                    &lt;span class="n"&gt;developers&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;developer&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                          &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'zsmb13'&lt;/span&gt;
                          &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Márton Braun'&lt;/span&gt;
                          &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'marton@getstream.io'&lt;/span&gt;
                        &lt;span class="o"&gt;}&lt;/span&gt;
                        &lt;span class="c1"&gt;// Add all other devs here...&lt;/span&gt;
                    &lt;span class="o"&gt;}&lt;/span&gt;

                    &lt;span class="c1"&gt;// Version control info - if you're using GitHub, follow the &lt;/span&gt;
                    &lt;span class="c1"&gt;// format as seen here&lt;/span&gt;
                    &lt;span class="n"&gt;scm&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'scm:git:github.com/getstream/stream-chat-android.git'&lt;/span&gt;
                        &lt;span class="n"&gt;developerConnection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'scm:git:ssh://github.com/getstream/stream-chat-android.git'&lt;/span&gt;
                        &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://github.com/getstream/stream-chat-android/tree/main'&lt;/span&gt;
                    &lt;span class="o"&gt;}&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, this piece of code grabs signing related values from the root project, and then tells the signing plugin to sign the artifacts you’ve defined above as a publication.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;signing&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;useInMemoryPgpKeys&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;rootProject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"signing.keyId"&lt;/span&gt;&lt;span class="o"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;rootProject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"signing.key"&lt;/span&gt;&lt;span class="o"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;rootProject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"signing.password"&lt;/span&gt;&lt;span class="o"&gt;],&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sign&lt;/span&gt; &lt;span class="n"&gt;publishing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publications&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the &lt;code&gt;publish-module.gradle&lt;/code&gt; script all built up, ready to use. &lt;a href="https://gist.github.com/zsmb13/56ed98c8fe916de441f2a9d8e060cd4a"&gt;This gist&lt;/a&gt; contains the code blocks shown above in a single snippet. The final, up-to-date script we use for publishing our SDK can be found &lt;strong&gt;&lt;a href="https://github.com/GetStream/stream-chat-android/blob/develop/scripts/publish-module.gradle"&gt;in the Stream Chat repository&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Time to include this script in a module! Head to the &lt;code&gt;build.gradle&lt;/code&gt; file of your library module - in our case, this is the &lt;code&gt;stream-chat-android-client&lt;/code&gt; module - and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;PUBLISH_GROUP_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'io.getstream'&lt;/span&gt;
    &lt;span class="n"&gt;PUBLISH_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'4.5.2'&lt;/span&gt;
    &lt;span class="n"&gt;PUBLISH_ARTIFACT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'stream-chat-android-client'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="nl"&gt;from:&lt;/span&gt; &lt;span class="s2"&gt;"${rootProject.projectDir}/scripts/publish-module.gradle"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here you finally see the group ID, artifact ID, and version being set, so that the publication script can make use of them. Then, the script itself is applied. This is all the code you need to add per-module if you are publishing your library in multiple artifacts, everything else is done by the common script.&lt;/p&gt;

&lt;p&gt;One last configuration step: you'll need to set the &lt;code&gt;sonatypeStagingProfileId&lt;/code&gt; variable in your project. This value is an ID that Sonatype assigns to you, which the plugin uses to make sure all the artifacts end up in the right place during the upload.&lt;/p&gt;

&lt;p&gt;Go to &lt;a href="https://oss.sonatype.org/"&gt;https://oss.sonatype.org/&lt;/a&gt; and log in. In the menu on the left, select &lt;em&gt;Staging profiles&lt;/em&gt;, select your profile, and then look for the ID in the URL.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vBMDc2hM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r9h8zemd3o9a3e97wim7.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vBMDc2hM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r9h8zemd3o9a3e97wim7.PNG" alt="The Sonatype staging profiles screen and the staging profile ID shown in the URL" width="750" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Either set this value in your &lt;code&gt;local.properties&lt;/code&gt; file, or set the corresponding environment variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your first release
&lt;/h2&gt;

&lt;p&gt;With all of that set up, you’re now ready to publish the first version of your library!&lt;/p&gt;

&lt;p&gt;For each repository you have defined in the publishing script, a Gradle task will be created to publish to that repository. In our example, our first module to publish is &lt;code&gt;stream-chat-android-client&lt;/code&gt;, and we’ve named the repository &lt;code&gt;sonatype&lt;/code&gt;. Therefore, we need to execute the following command to start publication (replace the module name with your own here):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gradlew stream-chat-android-client:publishReleasePublicationToSonatypeRepository
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create a so-called staging repository for your library, and upload your artifacts (&lt;code&gt;aar&lt;/code&gt; and sources) to that repository. At this point, you can check that all the artifacts you wanted to upload have made it, before hitting the release button.&lt;/p&gt;

&lt;p&gt;Now, go to the Sonatype site, and in the menu on the left, select &lt;em&gt;Staging repositories&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--WokTFpn_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sbzmk4tmkojusmrnyelg.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WokTFpn_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sbzmk4tmkojusmrnyelg.PNG" alt="The Sonatype menu" width="185" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Find your repository (might be the only one in the list, might have to scroll around a bit for it), which has your group ID in its name. If you select it and look at the &lt;em&gt;Content&lt;/em&gt; tab, you’ll see the files that have been uploaded.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--m9wOumow--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yb2bw6ft36pfs2dp0j4n.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--m9wOumow--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yb2bw6ft36pfs2dp0j4n.PNG" alt="The contents of the staging repository" width="643" height="688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If everything looks good, and you’re done uploading files to the repository, you have to &lt;em&gt;Close&lt;/em&gt; it. With the repository selected, hit the &lt;em&gt;Close&lt;/em&gt; button in the toolbar on top. Confirm your action in the dialog (you don’t need to provide a description here).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FLPn4l4q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4r3ugl82lr776wnfnwqc.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FLPn4l4q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4r3ugl82lr776wnfnwqc.PNG" alt="Closing a staging repository" width="410" height="243"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This will take just a few moments, you can follow along with it happening in the &lt;em&gt;Activity&lt;/em&gt; tab.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bPtXTGTY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ay61m9hewfnh10p5ce1c.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bPtXTGTY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ay61m9hewfnh10p5ce1c.PNG" alt="Repository Activity tab showing successful close operation" width="641" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the repository closed, you now have two final options available to you. &lt;em&gt;Drop&lt;/em&gt; will throw away the repository, and cancel the publication process. Use this if something went wrong during the upload or you’ve changed your mind.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Release&lt;/em&gt;, on the other hand, will publish the contents of your staging repository to MavenCentral. Again, you get a confirmation dialog, and you can choose &lt;em&gt;Automatically Drop&lt;/em&gt; so that the staging repository is cleaned up after the release completes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Za3CJXzn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pwh2i6c7nm6a8d0vue04.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Za3CJXzn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pwh2i6c7nm6a8d0vue04.PNG" alt="Release dialog for the staging repository" width="410" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The time this process takes can vary a bit. If you get lucky, your artifact will show up on MavenCentral in 10-15 minutes, but it could also take an hour or more in other cases. You can check whether your artifact is available by going to &lt;a href="https://repo1.maven.org/maven2/"&gt;https://repo1.maven.org/maven2/&lt;/a&gt; and browsing for it.&lt;/p&gt;

&lt;p&gt;For example, for our client, we can keep refreshing the following page to see if the new version is there: &lt;a href="https://repo1.maven.org/maven2/io/getstream/stream-chat-android-client/"&gt;https://repo1.maven.org/maven2/io/getstream/stream-chat-android-client/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you see the package via those links, Gradle will also be able to pull it if &lt;code&gt;mavenCentral()&lt;/code&gt; is added as a repository and you add your library as a dependency.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Search indexing is a separate, even longer process, so it can take about two hours for your artifact to show up on &lt;a href="https://search.maven.org/"&gt;search.maven.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If this was your first release, you should at this point go back and comment on your original Jira issue, to let them know that your repository setup and publication is working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating Sonatype actions
&lt;/h2&gt;

&lt;p&gt;That was quite the adventure! To make things smoother for subsequent releases, you can automate the entire release flow with the publishing plugin that you already have configured in the project.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This, in theory, would mean that you don’t have to revisit the Sonatype UI ever again. In practice, you’ll do that more often than you’d like when the plugin (really, the Sonatype API it calls) breaks or misbehaves, so it's worth being familiar with.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The plugin provides a Gradle task that you can use to close and then release your staging repository with one simple call:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;At this point, you can upload and publish your library by just invoking these two Gradle tasks in sequence - pretty convenient! As a final step, let’s hook this into a CI pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuous integration
&lt;/h2&gt;

&lt;p&gt;Since the library is hosted on GitHub anyway, we use GitHub Actions for running the publication Gradle tasks automatically. Whatever CI solution you’re using, setting up publication with it will consist of two main steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Getting your secret variables in place.&lt;/li&gt;
&lt;li&gt;Invoking the two Gradle tasks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your secret variables - for the list of these, look at the publishing script again - can simply go into Repository secrets (Add each of these by going to &lt;em&gt;Settings -&amp;gt; Secrets&lt;/em&gt; within your GitHub repository):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--M2YzhPvt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iw4hnbrqosc9tfw0cywx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--M2YzhPvt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iw4hnbrqosc9tfw0cywx.png" alt="GitHub secrets screen" width="800" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, let’s create the GitHub Actions workflow that will put all of this together. The configuration for this will go in the &lt;code&gt;.github/workflows/publish.yml&lt;/code&gt; file of the repository. This publish workflow will run every time a new &lt;a href="https://github.com/GetStream/stream-chat-android/releases"&gt;release&lt;/a&gt; is created in the repository (you can also change the triggers to run when a tag is created, for example). Again, see the inline comments for some explanation of what each step does.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you’re new to GitHub Actions, check out their &lt;a href="https://docs.github.com/en/actions"&gt;introductory documentation&lt;/a&gt; first.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# We'll run this workflow when a new GitHub release is created&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;released&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Release build and publish&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up JDK &lt;/span&gt;&lt;span class="m"&gt;11&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;distribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;adopt&lt;/span&gt;
          &lt;span class="na"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;11&lt;/span&gt;

        &lt;span class="c1"&gt;# Builds the release artifacts of the library&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Release build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./gradlew :stream-chat-android-client:assembleRelease&lt;/span&gt;

        &lt;span class="c1"&gt;# Generates other artifacts (javadocJar is optional)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Source jar and dokka&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./gradlew androidSourcesJar javadocJar&lt;/span&gt;

        &lt;span class="c1"&gt;# Runs upload, and then closes &amp;amp; releases the repository&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish to MavenCentral&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./gradlew publishReleasePublicationToSonatypeRepository --max-workers 1 closeAndReleaseSonatypeStagingRepository&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;OSSRH_USERNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.OSSRH_USERNAME }}&lt;/span&gt;
          &lt;span class="na"&gt;OSSRH_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.OSSRH_PASSWORD }}&lt;/span&gt;
          &lt;span class="na"&gt;SIGNING_KEY_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIGNING_KEY_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;SIGNING_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIGNING_PASSWORD }}&lt;/span&gt;
          &lt;span class="na"&gt;SIGNING_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIGNING_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;SONATYPE_STAGING_PROFILE_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SONATYPE_STAGING_PROFILE_ID }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MavenCentral uploads tend to be… Flaky, to put it nicely. Here are some recommendations to have fewer issues with them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Perform all your Gradle upload tasks in a single job, on a single machine.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;--max-workers 1&lt;/code&gt; option used above guarantees that the upload task runs on a single thread, even if parallel builds are otherwise configured.&lt;/li&gt;
&lt;li&gt;It’s also a good idea to put the upload task (all of them, if you’re uploading multiple modules’ worth of artifacts) in a different Gradle invocation than the assemble &amp;amp; signing tasks, as you see it done above, so that they run closely together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a long time passes between your upload calls, or if they originate from different CI machines, you might see problems such as multiple staging repositories being created for you on Sonatype with your files scattered all over them.&lt;/p&gt;

&lt;p&gt;Even if you do everything described above right, you might still get random upload errors, or timeouts either during the upload or while the plugin is trying to close and release the repository. At this point, being able to look at the staging repository on the Sonatype UI and manually closing/dropping/releasing repositories will come in handy to fix things up.&lt;/p&gt;

&lt;p&gt;If your staging repository seems okay (has correct contents), and it just failed to close, you can close and release it yourself. If it looks messed up somehow, or you ended up with multiple staging repositories, it’s best to drop them all and then run the upload workflow again.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Reminder: our up-to-date publishing workflow is always available &lt;a href="https://github.com/GetStream/stream-chat-android/blob/master/.github/workflows/publish.yml"&gt;in our GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Well, that was quite a journey. We hope that this detailed guide helped you get up and running with MavenCentral publication. You can follow us on Twitter &lt;a href="https://twitter.com/getstream_io"&gt;@getstream_io&lt;/a&gt; for more great technical content - &lt;strong&gt;please tweet at us if you've successfully set up publishing following these steps!&lt;/strong&gt; You can also reach the author &lt;a href="https://twitter.com/zsmb13"&gt;@zsmb13&lt;/a&gt; if you have any questions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you're looking for a Kotlin-first, powerful &lt;a href="https://getstream.io/chat/sdk/android/"&gt;Chat SDK&lt;/a&gt; to use in your Android app, check out &lt;a href="https://getstream.io/chat/docs/android/?language=kotlin"&gt;our Android documentation&lt;/a&gt; and our &lt;a href="https://getstream.io/tutorials/android-chat/"&gt;Android Chat tutorial&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're interested in library development, you'll find these articles helpful as well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://zsmb.co/maintaining-compatibility-in-kotlin-libraries/"&gt;Maintaining Compatibility in Kotlin Libraries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zsmb.co/mastering-api-visibility-in-kotlin/"&gt;Mastering API Visibility in Kotlin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>library</category>
      <category>mavencentral</category>
    </item>
    <item>
      <title>Pi Practice App in Compose</title>
      <dc:creator>Márton Braun</dc:creator>
      <pubDate>Mon, 19 Apr 2021 07:48:27 +0000</pubDate>
      <link>https://dev.to/zsmb13/pi-practice-app-in-compose-3ifo</link>
      <guid>https://dev.to/zsmb13/pi-practice-app-in-compose-3ifo</guid>
      <description>&lt;p&gt;&lt;em&gt;This article was originally planned for 🥧 &lt;a href="https://en.wikipedia.org/wiki/Pi_Day"&gt;Pi Day&lt;/a&gt;, and it clearly ran more than a month late. However, it's finally complete!&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Concept
&lt;/h2&gt;

&lt;p&gt;This project is once again one of my older projects revived with Jetpack Compose. A couple years ago, I wrote an app that lets you practice and learn the digits of pi - always a fun thing to spend your commute on.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm rather rusty now and down to knowing about ~60 digits. How high can you score in the app? Tweet at me &lt;a href="https://twitter.com/zsmb13"&gt;@zsmb13&lt;/a&gt;!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The app is still available in its original form &lt;a href="https://play.google.com/store/apps/details?id=co.zsmb.pi"&gt;on the Google Play Store&lt;/a&gt; for your pi practicing pleasure. Its sources are also available &lt;a href="https://github.com/zsmb13/Pi"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Zc1m2TK9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9lur55jts3k8782c244p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Zc1m2TK9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9lur55jts3k8782c244p.png" alt="The Pi app in the Play Store" width="701" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you take a look at the repository, you'll find something rather interesting in its dependencies. It uses &lt;a href="https://github.com/Kotlin/anko/wiki/Anko-Layouts"&gt;Anko Layouts&lt;/a&gt; to render some parts of its UI! This was an early, now deprecated and abandoned attempt by JetBrains to create declarative Android UI with Kotlin DSLs.&lt;/p&gt;

&lt;p&gt;In this article, we'll build the app in Compose, and also take a look at what some of the old Anko code looked like when we reach the relevant parts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The completed app's source code is available &lt;a href="https://github.com/zsmb13/PiCompose"&gt;on GitHub&lt;/a&gt;, so you can browse it freely while reading the article.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Project setup, navigation
&lt;/h3&gt;

&lt;p&gt;The app is rather simple, it consists of just two screens. The main screen contains the field displaying the current progress and the keypad to mash in the digits. The high scores screen displays the best runs you had at the digits of pi (a top 10 list of best attempts).&lt;/p&gt;

&lt;p&gt;Navigation setup is simple in &lt;code&gt;MainActivity&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;setContent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;PiPracticeComposeTheme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Surface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;navController&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberNavController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nc"&gt;NavHost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;startDestination&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"home"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;composable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"home"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nc"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="nf"&gt;composable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"highscores"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nc"&gt;HighScores&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Main screen
&lt;/h3&gt;

&lt;p&gt;The first building block of the main screen will be a composable that displays a button, which can notify us when it was clicked. We'll design this with a bottom-up approach.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;NumberButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;modifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;OutlinedButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nd"&gt;@OptIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ExperimentalStdlibApi&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digitToChar&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modifier&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aspectRatio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;border&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ButtonDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outlinedBorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;brush&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SolidColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;fontSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;code&gt;NumberButton&lt;/code&gt; uses basic Compose concepts, and is based on a material &lt;code&gt;OutlinedButton&lt;/code&gt;. It takes a &lt;code&gt;modifier&lt;/code&gt; as an optional parameter, following the relevant section of the &lt;a href="https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md#elements-accept-and-respect-a-modifier-parameter"&gt;Jetpack Compose API Guidelines&lt;/a&gt; by Google (a must-read if you're writing Compose code).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--icF8X4co--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9ezspna2hdk0zksekiw7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--icF8X4co--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9ezspna2hdk0zksekiw7.png" alt="A NumberButton" width="347" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Keypad
&lt;/h3&gt;

&lt;p&gt;The next step up is assembling a grid of these on the screen, adding a progress indicator in the bottom row, and adding the current value of pi that's been entered in a field on the top of the keypad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;KeyPad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scrollState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ScrollState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;
    &lt;span class="n"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 2&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;progress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;fontSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verticalScroll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scrollState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxWidth&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;NumberButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nc"&gt;NumberButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nc"&gt;NumberButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxWidth&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;NumberButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nc"&gt;NumberButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nc"&gt;NumberButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxWidth&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;NumberButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nc"&gt;NumberButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nc"&gt;NumberButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxWidth&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;verticalAlignment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Alignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CenterVertically&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;NumberButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// 3&lt;/span&gt;
                &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_progress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;textAlign&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TextAlign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;fontSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code is a bit wordy, but it mostly just creates all the rows of buttons we'll need, and aligns them using &lt;code&gt;weight&lt;/code&gt; modifiers. Let's review some of the other interesting bits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We'll make the top &lt;code&gt;Text&lt;/code&gt; that displays the current value of pi scrollable, as we'd eventually run out of space while practicing. This is easily done by using the &lt;a href="https://developer.android.com/reference/kotlin/androidx/compose/foundation/package-summary#verticalscroll"&gt;&lt;code&gt;verticalScroll&lt;/code&gt; modifier&lt;/a&gt;. Note how the scroll state is &lt;a href="https://developer.android.com/jetpack/compose/state#stateless-composables"&gt;hoisted&lt;/a&gt; to the containing function, which will let it control it externally.&lt;/li&gt;
&lt;li&gt;All number button clicks are passed to the same &lt;code&gt;onClick&lt;/code&gt; event listener.&lt;/li&gt;
&lt;li&gt;We use the &lt;a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/res/package-summary#stringresource"&gt;&lt;code&gt;stringResource&lt;/code&gt; function&lt;/a&gt; to access Android string resources to make the few UI strings we have translatable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is what we built so far:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LIC-FTAz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rmfmpnv5czd0spu2rsow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LIC-FTAz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rmfmpnv5czd0spu2rsow.png" alt="The completed keypad" width="800" height="1316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Game logic
&lt;/h3&gt;

&lt;p&gt;Now it's time to add the main game logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;PiPad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;pi&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;

    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberCoroutineScope&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;// 2&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;input&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"3."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;scrollState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberScrollState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;// 3&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;showDialog&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;lastInput&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"3."&lt;/span&gt;
        &lt;span class="n"&gt;lastInput&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="sc"&gt;'-'&lt;/span&gt;
        &lt;span class="n"&gt;showDialog&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;KeyPad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;scrollState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scrollState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;digit&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;// 4&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;indexToTest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;correctDigit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;indexToTest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

            &lt;span class="n"&gt;lastInput&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;digit&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digit&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;correctDigit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// 5&lt;/span&gt;
                &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;digit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;scrollState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;animateScrollTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scrollState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// 6&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;decimalReached&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
                &lt;span class="nc"&gt;ScoreKeeper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decimalReached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="n"&gt;showDialog&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;showDialog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;

        &lt;span class="nc"&gt;EndOfGameDialog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onDismiss&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// 7&lt;/span&gt;
            &lt;span class="nc"&gt;Column&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dialog_decimal_reached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dialog_digit_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
                &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dialog_next_digits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is quite a bit of code, but most of it is simple, and it handles all of the game logic for us!&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The true value of pi comes from string resources.&lt;/li&gt;
&lt;li&gt;The current value that the player has progressed to is always in the &lt;code&gt;input&lt;/code&gt; variable. We also create the scroll state of the view that will display this value on the top of the screen here.&lt;/li&gt;
&lt;li&gt;When the game ends, we'll show a dialog. This requires us to remember the last input digit, and to keep track of whether we need to show the dialog or not.&lt;/li&gt;
&lt;li&gt;We instantiate a &lt;code&gt;KeyPad&lt;/code&gt;, and in its &lt;code&gt;onClick&lt;/code&gt; listener, we implement all of the game logic.&lt;/li&gt;
&lt;li&gt;If the player inputs the correct digit, we'll add it to the &lt;code&gt;input&lt;/code&gt;, and make sure the view displaying it is scrolled to the bottom.&lt;/li&gt;
&lt;li&gt;If the player makes an error, we log the score they reached, and then display the end-of-game dialog.&lt;/li&gt;
&lt;li&gt;The dialog's content is passed in with the &lt;a href="https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md#compose-ui-layouts"&gt;&lt;code&gt;content&lt;/code&gt; lambda convention&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;ScoreKeeper&lt;/code&gt; is a relatively boring Kotlin object that reads/writes a file to keep track of the high scores. You can check out its implementation &lt;a href="https://github.com/zsmb13/PiCompose/blob/main/app/src/main/java/co/zsmb/picompose/data/ScoreKeeper.kt"&gt;in the GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Displaying a dialog
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;EndOfGameDialog&lt;/code&gt; function used above is a wrapper around &lt;code&gt;AlertDialog&lt;/code&gt;, implemented like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;EndOfGameDialog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;onDismiss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nd"&gt;@Composable&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;AlertDialog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;onDismissRequest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;onDismiss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dialog_game_over_title&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;confirmButton&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;onDismiss&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dialog_ok&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ye-bGlzM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jrbr335m3u1j1jan25e3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ye-bGlzM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jrbr335m3u1j1jan25e3.png" alt="The end-of-game dialog" width="800" height="546"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This dialog was one of the parts implemented with Anko in the original app, which looked &lt;a href="https://github.com/zsmb13/Pi/blob/94213675094a81dd1ec3cb96e86f7deb26d5bdb4/app/src/main/java/co/zsmb/pi/MainActivity.kt#L68"&gt;like this&lt;/a&gt; - quite familiar and easy to read!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;alert&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;customView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;verticalLayout&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;padding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="nf"&gt;textView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decimal_reached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indexReached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;textSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;16f&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nf"&gt;textView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;digit_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;correctDigit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;textSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;16f&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nf"&gt;textView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;nextDigit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;indexReached&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next_digits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digitsToShow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nextDigit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nextDigit&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;digitsToShow&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;textSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;16f&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nf"&gt;positiveButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OK&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;resetState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;onCancelled&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;resetState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the final touch for the main screen, we'll add a toolbar on top to display the app name and provide navigation to the second screen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;NavController&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topBar&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;TopAppBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Pi"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;IconButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"highscores"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nc"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="nc"&gt;Icons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Star&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content_description_high_scores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;PiPad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This now shows up on the top of our screen:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DCdWhugw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uheku8cpltqh22u1suos.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DCdWhugw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uheku8cpltqh22u1suos.png" alt="The main toolbar" width="800" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  High scores
&lt;/h3&gt;

&lt;p&gt;The high scores screen is extremely simple, it will display just a toolbar and a list of values. Before we see the Compose implementation, here's the original Anko code for this screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;verticalLayout&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;padding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;verticalGravity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Gravity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CENTER&lt;/span&gt;

    &lt;span class="nf"&gt;tableLayout&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;tableRow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;horizontalGravity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Gravity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CENTER&lt;/span&gt;
            &lt;span class="nf"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"# "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 2&lt;/span&gt;
            &lt;span class="nf"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Score&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;// 2&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nc"&gt;ScoreKeeper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEachIndexed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;
            &lt;span class="nf"&gt;tableRow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;horizontalGravity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Gravity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CENTER&lt;/span&gt;
                &lt;span class="nf"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${index + 1}."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 2&lt;/span&gt;
                &lt;span class="nf"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c1"&gt;// 2&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;The rows of the table are created with a simple &lt;code&gt;forEachIndexed&lt;/code&gt; loop - a huge advantage and convenience of writing declarative layouts in Kotlin, having all the language features and constructs like the collection APIs available.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Each row here was assembled by calls to the &lt;code&gt;cell&lt;/code&gt; function, an extension I added on the receiver scope of the Anko &lt;code&gt;tableRow&lt;/code&gt; function - the oddly named &lt;code&gt;_TableRow&lt;/code&gt; type.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;_TableRow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;textView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;padding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;textSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;20f&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Compose syntax for this screen will yet again be very similar to the Anko based approach. We'll set up a toolbar much like on the main screen, and then add a header and some items underneath in a loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;HighScores&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;NavController&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;topBar&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;TopAppBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;high_scores_title&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;navigationIcon&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nc"&gt;IconButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popBackStack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="nc"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Icons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArrowBack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;horizontalAlignment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Alignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CenterHorizontally&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;width&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

            &lt;span class="nc"&gt;ScoreKeeper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEachIndexed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="nc"&gt;ScoreItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;width&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Header&lt;/code&gt; and the &lt;code&gt;ScoreItem&lt;/code&gt; composables are extremely similar, they'll just use slightly different text styling. Here's just the latter of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;ScoreItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;modifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"${index}."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;textAlign&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TextAlign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;textAlign&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TextAlign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ta-da, now we can display the high scores in the app:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hITDbAef--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f3i528ss3l1rjg0k3zsn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hITDbAef--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f3i528ss3l1rjg0k3zsn.png" alt="The High Scores screen" width="800" height="751"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrap-up
&lt;/h3&gt;

&lt;p&gt;That's it for now! Thanks for following along with another Compose article. You can find the completed project &lt;a href=""&gt;on GitHub&lt;/a&gt; to play around with it and see the rest of the code that I omitted from the article.&lt;/p&gt;

&lt;p&gt;If you liked this project, you'll also like these other ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/zsmb13/build-an-android-chat-app-with-jetpack-compose-29k8"&gt;Build an Android Chat app with Jetpack Compose&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zsmb.co/compose-o-clock/"&gt;Compose O'Clock&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/zsmb13/prime-table-generator-in-jetpack-compose-2m0m"&gt;Prime Table Generator in Jetpack Compose&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>jetpackcompose</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>Prime Table Generator in Jetpack Compose</title>
      <dc:creator>Márton Braun</dc:creator>
      <pubDate>Thu, 25 Mar 2021 17:11:09 +0000</pubDate>
      <link>https://dev.to/zsmb13/prime-table-generator-in-jetpack-compose-2m0m</link>
      <guid>https://dev.to/zsmb13/prime-table-generator-in-jetpack-compose-2m0m</guid>
      <description>&lt;p&gt;It's been more than 6 years since I wrote an original version of this prime table generator. That was back at the very beginning of my coding career, after learning C and SDL in the first semester of university. An archived version of that original project &lt;a href="https://zsmbprogsite.netlify.app/projects/primetable/" rel="noopener noreferrer"&gt;is available here&lt;/a&gt;, including the sources, &lt;a href="https://zsmbprogsite.netlify.app/projects/primetable/primetable.c" rel="noopener noreferrer"&gt;120 lines of excellent C code&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Premise
&lt;/h3&gt;

&lt;p&gt;Recapping from the article linked above briefly: the point of this project is to create a visually pleasing and concise representation of prime numbers. The original, on-paper version contained prime numbers up to 4000, and looked like this:&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4h9n2951jxkr9wf0rge7.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4h9n2951jxkr9wf0rge7.jpg" alt="The full table"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How does it work? Each square represents a block of ten numbers. Since primes (above 2) may only end on the digits 1, 3, 7, or 9, each corner of the square can indicate whether or not a given ending digit is a prime within the 10 number wide block.&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F97io2wvnzx72mbb6xgqv.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F97io2wvnzx72mbb6xgqv.png" alt="Example of the first few squares of the table"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As an example, the third block of the table corresponds to the numbers 21-30, and the two connected corners indicate that only 23 and 29 are primes within this range.&lt;/p&gt;

&lt;p&gt;Now, let's get to coding this for Android!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can find the code for the completed project &lt;a href="https://github.com/zsmb13/PrimeTableGenerator" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Creating a grid
&lt;/h3&gt;

&lt;p&gt;In &lt;a href="https://zsmb.co/compose-o-clock/" rel="noopener noreferrer"&gt;the previous Jetpack Compose article on this blog&lt;/a&gt;, we created an animated clock with a bottom-up approach. This time, we'll design things top-down, and start with rendering a grid in Compose. For this, we'll use the experimental &lt;a href="https://developer.android.com/jetpack/compose/lists#grids" rel="noopener noreferrer"&gt;&lt;code&gt;LazyVerticalGrid&lt;/code&gt;&lt;/a&gt; APIs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@OptIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ExperimentalFoundationApi&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;Primes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;LazyVerticalGrid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;background&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFFE53935&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;cells&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GridCells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Fixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// 2&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 3&lt;/span&gt;
            &lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
                &lt;span class="nc"&gt;Modifier&lt;/span&gt; &lt;span class="c1"&gt;// 4&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aspectRatio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;background&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DarkGray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking down the code above:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;LazyVerticalGrid&lt;/code&gt; composable fills the entire screen, has a red background, and a small bit of padding.&lt;/li&gt;
&lt;li&gt;It displays a grid with a fixed number of columns.&lt;/li&gt;
&lt;li&gt;The grid contains 100 items.&lt;/li&gt;
&lt;li&gt;Each item is a simple &lt;code&gt;Box&lt;/code&gt; for a start, which is constrained to be a square shape, has a bit of padding, and a dark background colour.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Note how we had to opt-in to using the experimental API with the &lt;code&gt;@OptIn&lt;/code&gt; annotation, which also requires some additional project-level configuration to enable it. You can read more about this language feature in &lt;a href="https://zsmb.co/mastering-api-visibility-in-kotlin/#opt-in-apis" rel="noopener noreferrer"&gt;Mastering API Visibility in Kotlin&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Running the code above renders the (scrollable) grid of squares:&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1iexp8vihbbotdy50eyd.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1iexp8vihbbotdy50eyd.png" alt="A grid of dark squares"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A single square
&lt;/h3&gt;

&lt;p&gt;Let's refactor this a bit, and create a &lt;code&gt;PrimeSquare&lt;/code&gt; composable for each item of the grid. This will receive the current offset that it should render prime numbers for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@OptIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ExperimentalFoundationApi&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;Primes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;LazyVerticalGrid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="nc"&gt;PrimeSquare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;PrimeSquare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;Modifier&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aspectRatio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;background&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DarkGray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;CornerLine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the content of a single &lt;code&gt;PrimeSquare&lt;/code&gt;, we'll render just one line from the top left corner to the center, with our own &lt;code&gt;CornerLine&lt;/code&gt; composable. We can do this in Compose using the &lt;code&gt;Canvas&lt;/code&gt; API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;CornerLine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;drawLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;White&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Offset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;strokeWidth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toPx&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives us the following look - a good start!&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp7d3lv68xb08rlfvw02r.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp7d3lv68xb08rlfvw02r.png" alt="Prime squares with lines all in the top left corner"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Rotating and stacking squares
&lt;/h3&gt;

&lt;p&gt;To get this line into the correct corner, we can rotate the canvas while drawing on it. A simple &lt;code&gt;rotate&lt;/code&gt; function call takes care of this for us. We'll take the rotation amount as a parameter to &lt;code&gt;CornerLine&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;CornerLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;drawLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;White&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Offset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;strokeWidth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toPx&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make things super easy, we'll create a named composable for each corner, with the appropriate rotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;One&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CornerLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Composable&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;Three&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CornerLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;90f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Composable&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;Seven&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CornerLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;180f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Composable&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;Nine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CornerLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;270f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll have to know which number is a prime, for this we'll go with a very basic implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isPrime&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="n"&gt;until&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;none&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Have a shorter implementation for this that's at least as correct for checking primes? &lt;a href="https://twitter.com/zsmb13" rel="noopener noreferrer"&gt;Tweet it at me!&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now that we can check whether a number's a prime and can draw lines into each corner, we can implement &lt;code&gt;PrimeSquare&lt;/code&gt; trivially:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;PrimeSquare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;Modifier&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aspectRatio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;background&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DarkGray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isPrime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nc"&gt;One&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isPrime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nc"&gt;Three&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isPrime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nc"&gt;Seven&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isPrime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nc"&gt;Nine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, the way we're stacking Canvases here is not exactly optimal, but it's a good demonstration of how a &lt;code&gt;Box&lt;/code&gt; works as a container. If we moved the prime calculations a level lower, we could draw all our lines on a single Canvas for better performance - try doing this as a practice exercise.&lt;/p&gt;

&lt;p&gt;Still, our non-optimal implementation works well:&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxnfoijmvx2ke83g4le4z.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxnfoijmvx2ke83g4le4z.png" alt="A working implementation of the prime table"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Final touches
&lt;/h3&gt;

&lt;p&gt;There are two issues left here in our rendering, which you can spot if you look closely at the image above.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The ends of the lines drawn extend beyond the grey boxes.&lt;/li&gt;
&lt;li&gt;The lines meeting in the middle don't meet as expected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the first issue, we can make the &lt;code&gt;Box&lt;/code&gt; in the &lt;code&gt;PrimeSquare&lt;/code&gt; composable &lt;em&gt;clip&lt;/em&gt; to its bounds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Modifier&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aspectRatio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;background&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DarkGray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clipToBounds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make the lines overlap more in the middle, we can draw them just ever so slightly longer - &lt;code&gt;2f&lt;/code&gt; seems to do the trick:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;2f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;2f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the app again gives us our final result.&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1sl4dpsvily4srgx6lc9.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1sl4dpsvily4srgx6lc9.png" alt="The final version of the prime table"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Again, the completed source for this project is available &lt;a href="https://github.com/zsmb13/PrimeTableGenerator" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're looking for more similar Compose content, check out these articles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://zsmb.co/compose-o-clock/" rel="noopener noreferrer"&gt;Compose O'Clock&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/zsmb13/jetpack-compose-first-impressions-and-learning-resources-3ghp"&gt;Jetpack Compose: First Impressions and Learning Resources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://proandroiddev.com/android-chat-app-jetpack-compose-dec472140ff1" rel="noopener noreferrer"&gt;Build an Android Chat app with Jetpack Compose&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>jetpackcompose</category>
    </item>
    <item>
      <title>Build an Android Chat app with Jetpack Compose</title>
      <dc:creator>Márton Braun</dc:creator>
      <pubDate>Fri, 19 Mar 2021 13:42:18 +0000</pubDate>
      <link>https://dev.to/zsmb13/build-an-android-chat-app-with-jetpack-compose-29k8</link>
      <guid>https://dev.to/zsmb13/build-an-android-chat-app-with-jetpack-compose-29k8</guid>
      <description>&lt;h2&gt;
  
  
  Intro and context
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/zsmb13/jetpack-compose-first-impressions-and-learning-resources-3ghp"&gt;our previous article&lt;/a&gt; about Jetpack Compose, we shared our initial impressions and recommended some learning resources. This time, we'll take Compose out for a spin by building a basic chat UI implementation with it!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Stream now provides a &lt;a href="https://getstream.io/chat/sdk/compose/" rel="noopener noreferrer"&gt;Jetpack Compose Chat SDK&lt;/a&gt;. Check out the &lt;a href="https://getstream.io/chat/compose/tutorial/" rel="noopener noreferrer"&gt;Compose Chat Messaging Tutorial&lt;/a&gt; and give it a try today!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Of course, you might be wondering why we're building a Compose Chat app when there's an official Google sample, &lt;a href="https://github.com/android/compose-samples/tree/main/Jetchat" rel="noopener noreferrer"&gt;Jetchat&lt;/a&gt;, which is also a chat app using Compose. That sample is a basic UI demo with a small bit of hardcoded data. Here, we're building a real, functional chat app instead that's hooked up to a real-time backend service.&lt;/p&gt;

&lt;p&gt;We already ship a &lt;a href="https://github.com/GetStream/stream-chat-android/tree/main/stream-chat-android-ui-components" rel="noopener noreferrer"&gt;UI Components library&lt;/a&gt; in our Chat SDK which contains ready-to-go, fully-featured Android Views to drop into your app, along with ViewModels that let you connect them to business logic in just a single line of code. You can check out how that works in our &lt;a href="https://getstream.io/tutorials/android-chat/" rel="noopener noreferrer"&gt;Android Chat Tutorial&lt;/a&gt;.&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feubnxjmycyivkjos9vnv.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feubnxjmycyivkjos9vnv.png" alt="The Stream Chat UI Components in action"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time, we're going to reuse the ViewModels (and a View) from that implementation, and build our Compose-based UI on top of that. This won't be an ideal scenario, as those ViewModels are designed to work with the Views that they ship with.&lt;/p&gt;

&lt;p&gt;Having dedicated support for Compose in the SDK would make this much nicer and simpler, and that's something we'll be working on in the future. However, as you'll see, it's already quite easy to integrate Stream Chat with Jetpack Compose, without any specific support for it!&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftf4wy0j13eac8hyim9xr.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftf4wy0j13eac8hyim9xr.png" alt="The Compose based chat implementation we'll build"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project we're building in this article is  &lt;a href="https://github.com/zsmb13/stream-chat-compose-experiment" rel="noopener noreferrer"&gt;available on GitHub&lt;/a&gt;, feel free to clone the project and play around with it on your own! We recommend getting your own Stream API key for this, which you can do by &lt;a href="https://getstream.io/try-for-free/" rel="noopener noreferrer"&gt;signing up for a free trial&lt;/a&gt;. Stream is free for hobby projects and small companies even beyond the trial - just apply for a &lt;a href="https://getstream.io/maker-account/" rel="noopener noreferrer"&gt;free Maker account&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project setup
&lt;/h2&gt;

&lt;p&gt;To use Jetpack Compose, we'll create our project in the &lt;a href="https://developer.android.com/studio/preview" rel="noopener noreferrer"&gt;latest Canary version of Android Studio&lt;/a&gt;. This contains a project template for Compose apps.&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F86fmvejfrtfbkph4ap9v.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F86fmvejfrtfbkph4ap9v.png" alt="Jetpack Compose project template in Android Studio"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After creating the project, our first step will be to add the Stream UI Components SDK as a dependency, as well as some Jetpack Compose dependencies we need. Some of our dependencies come from Jitpack, so make sure to add that repository in the &lt;code&gt;settings.gradle&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="n"&gt;dependencyResolutionManagement&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;repositories&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
        &lt;span class="n"&gt;maven&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="s2"&gt;"https://jitpack.io"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// Add Jitpack&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in the module's &lt;code&gt;build.gradle&lt;/code&gt; file, replace the &lt;code&gt;dependencies&lt;/code&gt; block with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="k"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Stream Chat UI Components&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s2"&gt;"io.getstream:stream-chat-android-ui-components:4.8.1"&lt;/span&gt;

    &lt;span class="c1"&gt;// Google / Jetpack dependencies&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s2"&gt;"androidx.compose.runtime:runtime:$compose_version"&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s2"&gt;"androidx.compose.runtime:runtime-livedata:$compose_version"&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.core:core-ktx:1.3.2'&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.appcompat:appcompat:1.2.0'&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'com.google.android.material:material:1.3.0'&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s2"&gt;"androidx.compose.ui:ui:$compose_version"&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s2"&gt;"androidx.compose.material:material:$compose_version"&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s2"&gt;"androidx.compose.ui:ui-tooling:$compose_version"&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0'&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s2"&gt;"androidx.navigation:navigation-compose:1.0.0-alpha10"&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.activity:activity-compose:1.3.0-alpha06'&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha04'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Check out &lt;a href="https://github.com/zsmb13/stream-chat-compose-experiment" rel="noopener noreferrer"&gt;the GitHub project&lt;/a&gt; if you get tangled up in the dependencies.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next, we'll set up the Stream Chat SDK, providing an API key and user details to it. This is normally done in your custom &lt;code&gt;Application&lt;/code&gt; class. To make things simple, we'll use the same environment and user as &lt;a href="https://getstream.io/tutorials/android-chat/" rel="noopener noreferrer"&gt;our tutorial&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatApplication&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;appContext&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"b67pax5b2wdq"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tutorial-droid"&lt;/span&gt;
            &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://bit.ly/2TIt8NR"&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Tutorial Droid"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nc"&gt;ChatDomain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;offlineEnabled&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connectUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidHV0b3JpYWwtZHJvaWQifQ.NhEr0hP9W9nwqV7ZkdShxvi02C5PR7SJE7Cs4y7kyqg"&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're initializing all three layers of our SDK here: the low-level client, the domain providing offline support, and the UI components. For more info about these, take a look at &lt;a href="https://getstream.io/chat/docs/android/?language=kotlin" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt;. Finally, we're connecting our user, using a hardcoded, non-expiring example token from the tutorial.&lt;/p&gt;

&lt;p&gt;Since we created a custom &lt;code&gt;Application&lt;/code&gt; class, we have to update our Manifest to use this &lt;code&gt;ChatApplication&lt;/code&gt; when the app is launched. We'll also set &lt;code&gt;windowSoftInputMode&lt;/code&gt; on the &lt;code&gt;MainActivity&lt;/code&gt; so that we'll have better keyboard behaviour later on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;manifest&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;application&lt;/span&gt;
        &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;".ChatApplication"&lt;/span&gt;
        &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;activity&lt;/span&gt;
            &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;".MainActivity"&lt;/span&gt;
            &lt;span class="err"&gt;...&lt;/span&gt;
            &lt;span class="na"&gt;android:windowSoftInputMode=&lt;/span&gt;&lt;span class="s"&gt;"adjustResize"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            ...
        &lt;span class="nt"&gt;&amp;lt;/activity&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/application&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/manifest&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Channel List Screen
&lt;/h2&gt;

&lt;p&gt;First, we'll create a screen that lists the channels available to our user. The UI Components library contains a &lt;a href="https://getstream.io/chat/docs/android/channel_list_view/?language=kotlin" rel="noopener noreferrer"&gt;ChannelListView component&lt;/a&gt; for this purpose, and we can reuse its ViewModel here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;ChannelListScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;channelListViewModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ChannelListViewModel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;
        &lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChannelListViewModelFactory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;state&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;channelListViewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observeAsState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;channelState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="c1"&gt;// 2&lt;/span&gt;

    &lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// 3&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;contentAlignment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Alignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channelState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;CircularProgressIndicator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// 4&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;LazyColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 5&lt;/span&gt;
                &lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channelState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                    &lt;span class="nc"&gt;ChannelListItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nc"&gt;Divider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's see what this code does step-by-step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We use the &lt;code&gt;viewModel()&lt;/code&gt; method provided by Compose to get a ViewModel for our current context. This allows us to specify a factory that creates a ViewModel, which we need in the case of &lt;code&gt;ChannelListViewModel&lt;/code&gt; to pass in some parameters to it. We'll stick to the default parameters of the SDK's &lt;code&gt;ChannelListViewModelFactory&lt;/code&gt; for now, which will filter for channels that our current user is a member of.&lt;/li&gt;
&lt;li&gt;We observe the &lt;code&gt;LiveData&lt;/code&gt; state in the ViewModel as a composable &lt;code&gt;State&lt;/code&gt;, and we copy it to a local variable so that smart casts will work on it correctly. We also skip rendering anything if this &lt;code&gt;State&lt;/code&gt; happens to be &lt;code&gt;null&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We'll have the &lt;code&gt;ChannelListScreen&lt;/code&gt; fill the entire screen, and center any content inside it.&lt;/li&gt;
&lt;li&gt;If the state is loading, we'll display a basic progress indicator.&lt;/li&gt;
&lt;li&gt;Otherwise, we'll display the list of channels in a &lt;code&gt;LazyColumn&lt;/code&gt;. Each item will be a &lt;code&gt;ChannelListItem&lt;/code&gt; responsible for showing info for a single channel. We're also adding a simple &lt;code&gt;Divider&lt;/code&gt; after each item.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, let's see how we can render a single &lt;code&gt;ChannelListItem&lt;/code&gt; in the list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;ChannelListItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;horizontal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vertical&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxWidth&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;verticalAlignment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Alignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CenterVertically&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Avatar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 2&lt;/span&gt;
        &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 3&lt;/span&gt;
            &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDisplayName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LocalContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TextStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fontWeight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FontWeight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Bold&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;fontSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;lastMessageText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastOrNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s"&gt;"..."&lt;/span&gt;
            &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lastMessageText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;maxLines&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;overflow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TextOverflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Ellipsis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;We're rendering each item as a &lt;code&gt;Row&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Inside that, we're adding an &lt;code&gt;Avatar&lt;/code&gt; representing the &lt;code&gt;Channel&lt;/code&gt; at the start.&lt;/li&gt;
&lt;li&gt;Then we add a &lt;code&gt;Column&lt;/code&gt;, laying out two pieces of &lt;code&gt;Text&lt;/code&gt; vertically. These will contain the channel's title and a preview of the latest message. Note the &lt;code&gt;LocalContext.current&lt;/code&gt; API being used to grab a &lt;code&gt;Context&lt;/code&gt; in Compose, as well as the &lt;code&gt;TextOverflow.Ellipsis&lt;/code&gt; option that will make long messages behave nicely in the preview.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We have one last Composable to implement, the &lt;code&gt;Avatar&lt;/code&gt; used above. Our &lt;a href="https://github.com/GetStream/stream-chat-android/tree/main/stream-chat-android-ui-components" rel="noopener noreferrer"&gt;UI components SDK&lt;/a&gt; ships an &lt;code&gt;AvatarView&lt;/code&gt; with complex rendering logic inside it, which we don't want to reimplement for now. We can use &lt;a href="https://developer.android.com/jetpack/compose/interop" rel="noopener noreferrer"&gt;Jetpack Compose's interop features&lt;/a&gt; to include our regular Android &lt;code&gt;View&lt;/code&gt; inside Compose UI.&lt;/p&gt;

&lt;p&gt;This will be really simple, just a few lines of code to create a wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;Avatar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;AndroidView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;AvatarView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setChannelData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;AndroidView&lt;/code&gt; function allows us to create... Well, an Android &lt;code&gt;View&lt;/code&gt;, which we get a &lt;code&gt;Context&lt;/code&gt; for. It also gives us a way to hook into Compose's state updates using its &lt;code&gt;update&lt;/code&gt; parameter, which will be executed whenever composable state (in our case, the &lt;code&gt;Channel&lt;/code&gt; object) changes. When that happens, we set the new &lt;code&gt;Channel&lt;/code&gt; on our &lt;code&gt;AvatarView&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Finally, we can add the &lt;code&gt;ChannelListScreen&lt;/code&gt; to &lt;code&gt;MainActivity&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainActivity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppCompatActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bundle&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;setContent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;ComposeChatTheme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Surface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nc"&gt;ChannelListScreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Building and running this code gives us a working list of channels loaded from Stream's backend, with performant scrolling thanks to &lt;code&gt;LazyColumn&lt;/code&gt;. Not bad for a hundred lines of code for the entire thing!&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpn8rntay6kbs06fn3cnr.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpn8rntay6kbs06fn3cnr.png" alt="The Channel List screen showing the list of available channels"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up navigation
&lt;/h2&gt;

&lt;p&gt;Continuing with our chat app, let's create a new screen, where we'll display the list of messages in a channel. For this, we'll need to set up click listeners and navigation. We'll use the &lt;a href="https://developer.android.com/jetpack/compose/navigation" rel="noopener noreferrer"&gt;Navigation Component for Jetpack Compose&lt;/a&gt; here.&lt;/p&gt;

&lt;p&gt;Step one, we'll make our &lt;code&gt;ChannelListItem&lt;/code&gt; clickable, and add an &lt;code&gt;onClick&lt;/code&gt; parameter that it will call when it was clicked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;ChannelListItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// New callback parameter&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clickable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// Add clickablity&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;horizontal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vertical&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxWidth&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ChannelListScreen&lt;/code&gt; will take a &lt;code&gt;NavController&lt;/code&gt; as its parameter, and use it to navigate to a new destination when an item was clicked, based on the &lt;code&gt;cid&lt;/code&gt; of the channel.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;ChannelListScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;NavController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// New parameter&lt;/span&gt;
    &lt;span class="n"&gt;channelListViewModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ChannelListViewModel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChannelListViewModelFactory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
        &lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channelState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="nc"&gt;ChannelListItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"messagelist/${channel.cid}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nc"&gt;Divider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll update &lt;code&gt;MainActivity&lt;/code&gt; by moving our top-level Composable code to a &lt;code&gt;ChatApp&lt;/code&gt; composable, and setting up navigation in there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainActivity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppCompatActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bundle&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;setContent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;ComposeChatTheme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Surface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nc"&gt;ChatApp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;ChatApp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;navController&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberNavController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;
    &lt;span class="nc"&gt;NavHost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;startDestination&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"channellist"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;composable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"channellist"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 2&lt;/span&gt;
            &lt;span class="nc"&gt;ChannelListScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;navController&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;composable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"messagelist/{cid}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;backStackEntry&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;// 3&lt;/span&gt;
            &lt;span class="nc"&gt;MessageListScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;navController&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;cid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;backStackEntry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cid"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!!&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;We're creating a &lt;code&gt;NavController&lt;/code&gt; and a &lt;code&gt;NavHost&lt;/code&gt; for our application.&lt;/li&gt;
&lt;li&gt;Our first and initial destination is a composable: &lt;code&gt;ChannelListScreen&lt;/code&gt;. We pass the nav controller to it.&lt;/li&gt;
&lt;li&gt;Our second destination will be a new composable called &lt;code&gt;MessageListScreen&lt;/code&gt;. Here, we pass in both the nav controller and the argument that was used to navigate to it. We can grab this from the &lt;code&gt;NavBackStackEntry&lt;/code&gt; parameter.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Message List Screen
&lt;/h2&gt;

&lt;p&gt;Our message list will be a &lt;code&gt;LazyColumn&lt;/code&gt; similar to the channel list we created before. For displaying messages, the UI Components library gives us a &lt;code&gt;MessageListViewModelFactory&lt;/code&gt; which can create a few different ViewModel instances. This takes the &lt;code&gt;cid&lt;/code&gt; of the channel we want to display.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;MessageListScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;NavController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MessageListViewModelFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;MessageList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;navController&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll implement the &lt;code&gt;MessageList&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;MessageList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;NavController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MessageListViewModelFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;modifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messageListViewModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MessageListViewModel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;state&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;messageListViewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observeAsState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// 2&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;messageState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;contentAlignment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Alignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Center&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messageState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 3&lt;/span&gt;
            &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;MessageListViewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Loading&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;CircularProgressIndicator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;MessageListViewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NavigateUp&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popBackStack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;MessageListViewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;messageItems&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messageState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messageListItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="c1"&gt;// 4&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filterIsInstance&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MessageListItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MessageItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNotBlank&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asReversed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

                &lt;span class="nc"&gt;LazyColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                    &lt;span class="n"&gt;reverseLayout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 5&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messageItems&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                        &lt;span class="nc"&gt;MessageCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 6&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's review:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We get a &lt;code&gt;MessageListViewModel&lt;/code&gt; from the factory, which will fetch messages for us, and expose it as &lt;code&gt;LiveData&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;As before, we convert the &lt;code&gt;LiveData&lt;/code&gt; state from the ViewModel into composable &lt;code&gt;State&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We have three states to handle, defined by a sealed class inside &lt;code&gt;MessageListViewModel&lt;/code&gt;. The first state is a loading state, the second is a state that pushes us to the previous screen (using the nav controller), and the third is the result state, where we have a list of messages.&lt;/li&gt;
&lt;li&gt;In the result state, we'll grab the list of messages received, filter for only &lt;code&gt;MessageItem&lt;/code&gt;s (excluding things such as date separators that we don't want to render for now). We also filter for messages that have non-blank text - for example, some messages might have only images attached to them, so we'll skip those for simplicity. Finally, we reverse the list, to match what we're doing in the next step.&lt;/li&gt;
&lt;li&gt;We use &lt;code&gt;reverseLayout&lt;/code&gt; on &lt;code&gt;LazyColumn&lt;/code&gt; to stack its items from the bottom. This is similar to using &lt;code&gt;stackFromEnd&lt;/code&gt; on a &lt;code&gt;RecyclerView&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Each item will be rendered by a &lt;code&gt;MessageCard&lt;/code&gt; composable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a single message, we'll use the following layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;MessageCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messageItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MessageListItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MessageItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;
    &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxWidth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;horizontal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vertical&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;horizontalAlignment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 2&lt;/span&gt;
            &lt;span class="n"&gt;messageItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isMine&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Alignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;End&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Alignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Start&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;widthIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;340&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;shape&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cardShapeFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messageItem&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// 3&lt;/span&gt;
            &lt;span class="n"&gt;backgroundColor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;messageItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isMine&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;
                &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secondary&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messageItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;messageItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isMine&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onPrimary&lt;/span&gt;
                    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onSecondary&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// 4&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messageItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;fontSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;cardShapeFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MessageListItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MessageItem&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Shape&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;roundedCorners&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RoundedCornerShape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isMine&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;roundedCorners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bottomEnd&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CornerSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;roundedCorners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bottomStart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CornerSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code is mostly straightforward, but let's review some of its important bits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We take a &lt;code&gt;MessageItem&lt;/code&gt; as a parameter.&lt;/li&gt;
&lt;li&gt;Depending on whether this is our current user's message, or someone else's, we align it to one of the sides of the screen. We also set colours based on this in a few places.&lt;/li&gt;
&lt;li&gt;We use a &lt;code&gt;cardShapeFor&lt;/code&gt; helper method to create the shape of the &lt;code&gt;Card&lt;/code&gt; that holds the message. This will create a shape with rounded corners, except for one of the bottom corners, giving us a chat bubble look.&lt;/li&gt;
&lt;li&gt;We display the username under each message, so that we can distinguish between messages sent by others.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At this point, we can build and run again, and clicking a channel will navigate to the new screen, displaying the list of messages in it.&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsx0iw0yrftjljqb7q3ah.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsx0iw0yrftjljqb7q3ah.png" alt="The Message List Screen with a list of messages"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Message Input
&lt;/h2&gt;

&lt;p&gt;For our final piece of Jetpack Compose chat implementation, we'll add an input view on the message list screen so that we can send new messages.&lt;/p&gt;

&lt;p&gt;First, we'll modify &lt;code&gt;MessageListScreen&lt;/code&gt; and place a new composable under &lt;code&gt;MessageList&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;MessageListScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;NavController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MessageListViewModelFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nc"&gt;MessageList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;navController&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;navController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;MessageInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;weight&lt;/code&gt; modifier on &lt;code&gt;MessageList&lt;/code&gt; will make it take up the maximum available space above the new &lt;code&gt;MessageInput&lt;/code&gt;. We pass in the &lt;code&gt;factory&lt;/code&gt; to &lt;code&gt;MessageInput&lt;/code&gt; as well, so that it's able to access the currently open channel's data via ViewModels.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;MessageInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MessageListViewModelFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messageInputViewModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MessageInputViewModel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;inputValue&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// 2&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 3&lt;/span&gt;
        &lt;span class="n"&gt;messageInputViewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;inputValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;Row&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// 4&lt;/span&gt;
            &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;onValueChange&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;inputValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;keyboardOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KeyboardOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imeAction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ImeAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;keyboardActions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KeyboardActions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// 5&lt;/span&gt;
            &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;height&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNotBlank&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// 6&lt;/span&gt;
                &lt;span class="n"&gt;imageVector&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Icons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;contentDescription&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cd_button_send&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;This time, we'll use a &lt;code&gt;MessageInputViewModel&lt;/code&gt;, which normally belongs to the &lt;a href="https://getstream.io/chat/docs/android/message_input_view/?language=kotlin" rel="noopener noreferrer"&gt;MessageInputView&lt;/a&gt; shipped in the UI components library, and handles input actions.&lt;/li&gt;
&lt;li&gt;We create a piece of composable state to hold the current input value.&lt;/li&gt;
&lt;li&gt;This local helper function calls into the ViewModel and sends the message to the server. Then, it clears the input field by resetting the state.&lt;/li&gt;
&lt;li&gt;We use a &lt;code&gt;TextField&lt;/code&gt; to capture user input. This displays the current &lt;code&gt;inputValue&lt;/code&gt;, and modifies it based on keyboard input. We also set up IME options so that our software keyboard displays a &lt;em&gt;Send&lt;/em&gt; button, and we handle that being tapped by calling &lt;code&gt;sendMessage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We're also adding a &lt;code&gt;Button&lt;/code&gt; that the user can tap to send a message. This is enabled/disabled dynamically based on the current input value.&lt;/li&gt;
&lt;li&gt;The button will show an icon from the default Material icon set. Not to skimp on accessibility, we also add a content description string for the send icon. We grab this from regular Android resources using &lt;code&gt;stringResource&lt;/code&gt;, which allows us to localize it as usual. &lt;em&gt;Make sure to create this resource in your project.&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's build and run for the last time, and we have a working chat app now! We can browse channels, open them, read messages, and send new messages.&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4yfm0wlq73tmxm0xj41c.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4yfm0wlq73tmxm0xj41c.png" alt="The Message List Screen, with input support added"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;This complete implementation, including UI, logic, and a connection to a real server is roughly 250 lines of code in total. Almost all of this is UI code with Jetpack Compose, the integration with Stream Chat's ViewModels and Views is just a small fraction of it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow us on Twitter &lt;a href="https://twitter.com/getstream_io" rel="noopener noreferrer"&gt;@getstream_io&lt;/a&gt;, and the author &lt;a href="https://twitter.com/zsmb13" rel="noopener noreferrer"&gt;@zsmb13&lt;/a&gt; for more content like this. If you liked this tutorial, tweet at us and let us know!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As a reminder, you can check out the full project and play with it &lt;a href="https://github.com/zsmb13/stream-chat-compose-experiment" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;. We recommend expanding this project to learn more about Compose - a good first goal could be to add a login screen, so that you can have different users on different devices.&lt;/p&gt;

&lt;p&gt;To jump into that, sign up for a &lt;a href="https://getstream.io/try-for-free/" rel="noopener noreferrer"&gt;free trial of Stream Chat&lt;/a&gt;. If you're doing this for a side project or you're a small business, you can use our SDK for with a &lt;a href="https://getstream.io/blog/maker-account/" rel="noopener noreferrer"&gt;free Maker account&lt;/a&gt;, even beyond the trial period!&lt;/p&gt;

&lt;p&gt;Here are some more useful links to continue exploring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://github.com/GetStream/stream-chat-android" rel="noopener noreferrer"&gt;Stream Chat Android SDK on GitHub&lt;/a&gt;, including the &lt;a href="https://github.com/GetStream/stream-chat-android/tree/main/stream-chat-android-ui-components-sample" rel="noopener noreferrer"&gt;UI Components Sample app&lt;/a&gt; that showcases many features, and includes a login implementation you can take ideas from&lt;/li&gt;
&lt;li&gt;Our regular, non-Compose &lt;a href="https://getstream.io/tutorials/android-chat/" rel="noopener noreferrer"&gt;Android Tutorial&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://getstream.io/chat/docs/android/?language=kotlin" rel="noopener noreferrer"&gt;Stream Chat Android documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;For more about Compose, some &lt;a href="https://dev.to/zsmb13/jetpack-compose-first-impressions-and-learning-resources-3ghp"&gt;first impressions and recommended learning resources&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Originally published on &lt;a href="https://getstream.io/blog/android-jetpack-compose-chat-example/" rel="noopener noreferrer"&gt;getstream.io/blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>android</category>
      <category>chat</category>
      <category>kotlin</category>
      <category>jetpackcompose</category>
    </item>
    <item>
      <title>Jetpack Compose: First Impressions and Learning Resources</title>
      <dc:creator>Márton Braun</dc:creator>
      <pubDate>Fri, 05 Mar 2021 18:28:56 +0000</pubDate>
      <link>https://dev.to/zsmb13/jetpack-compose-first-impressions-and-learning-resources-3ghp</link>
      <guid>https://dev.to/zsmb13/jetpack-compose-first-impressions-and-learning-resources-3ghp</guid>
      <description>&lt;p&gt;As you most certainly already know, Jetpack Compose is the hot new thing for Android UI development. Its promise is to save us by making all the pain points of the old View system and XML layouts go away.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Stream now provides a &lt;a href="https://getstream.io/chat/sdk/compose/" rel="noopener noreferrer"&gt;Jetpack Compose Chat SDK&lt;/a&gt;. Check out the &lt;a href="https://getstream.io/chat/compose/tutorial/" rel="noopener noreferrer"&gt;Compose Chat Messaging Tutorial&lt;/a&gt; and give it a try today!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After many many preview versions, it got to beta status about a week ago. Here's what it's like to use Compose at the moment, and some of the best places to learn more about it.&lt;/p&gt;

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

&lt;p&gt;First of all, Compose is &lt;em&gt;exciting&lt;/em&gt;. It makes a lot of basic UI work really simple, and you get to write it all in Kotlin, making use of all the language's powerful features! The APIs have great discoverability in Android Studio, since all of it is just Kotlin code that you can access via code completion. This makes it truly easy to get started.&lt;/p&gt;

&lt;p&gt;Kotlin's type safety also works wonders in Compose. No passing in &lt;code&gt;dp&lt;/code&gt; values instead of pixels, or mixing up vertical and horizontal alignment options. Everything is strongly typed, and the compiler safeguards you from making many, many mistakes.&lt;/p&gt;

&lt;p&gt;The declarative style and state management of Compose are great as well. Writing down what you want to appear based on state is not only clear and explicit, but also a blessing after the problematic state management of the View system.&lt;/p&gt;

&lt;p&gt;Given how much effort is going into developing and marketing Compose, it's almost certainly the future of Android UI development. That being said, the XML system will probably never fully go away. Just like the Java to Kotlin shift, it will take a couple years until the new standard really kicks in, and we'll occasionally still find ourselves interacting with code from the &lt;em&gt;before times&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So... Get on board soon, just don't expect all of the old system to disappear overnight. I'd certainly still suggest learning the old XML-based system to anyone starting Android Development today - unless they're somehow &lt;em&gt;very&lt;/em&gt; certain that they'll never work on an existing project.&lt;/p&gt;

&lt;p&gt;Jetpack Compose comes with some cool Android Studio tooling as well. Flutter users will probably already be familiar with the handy &lt;em&gt;Surround with&lt;/em&gt; shortcuts that let you wrap pieces of code in a new widget, and the same makes an appearance in Studio now.&lt;/p&gt;

&lt;p&gt;Select some code, invoke &lt;em&gt;Surround with&lt;/em&gt; (Ctrl+Alt+J on Windows, ⌥⌘J on macOS), and then hit R for a &lt;code&gt;Row&lt;/code&gt;, C for a &lt;code&gt;Column&lt;/code&gt;, or W for an arbitrary widget.&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh1kguprkjt2d1xdep0a7.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh1kguprkjt2d1xdep0a7.png" alt="The Surround With popup with options"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another quick tip: creating a new Composable will immediately add a parameter list and a body to it, and then place your cursor inside its parameter list, to let you immediately add modifiers and other parameters.&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F20zb8sdbk59c58cv5qz8.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F20zb8sdbk59c58cv5qz8.png" alt="Creating a new Column"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;If you don't want to add any parameters, you can hit &lt;code&gt;Tab&lt;/code&gt; to quickly skip to defining its content.&lt;/p&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs7loojn2c4eak4dhltor.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs7loojn2c4eak4dhltor.png" alt="Creating a new Column after skipping to its body"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Now, let's see the bad part, continuing with the tooling theme. To run Compose, you'll have to run the latest Canary version of Android Studio. This version has... Some issues.&lt;/p&gt;

&lt;p&gt;From my personal experience of a few days:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IDE Previews (which would be a really nice feature for Compose) sometimes don't load at all, or don't update as changes are made (even after rebuilding). Re-running either the full app or just deploying a &lt;code&gt;@Preview&lt;/code&gt; to a device was faster than using the IDE preview feature 100% of the time.&lt;/li&gt;
&lt;li&gt;The debugger never connected to either an emulator or a physical device when running Compose apps. Still didn't manage to solve this, though I heard others had more success.&lt;/li&gt;
&lt;li&gt;Logcat doesn't show app names when running apps, making it difficult to filter the logs for the current app.&lt;/li&gt;
&lt;li&gt;When running the emulator in a tool window inside the IDE, the emulator tab occasionally doesn't appear in the preview version of Studio. I had times when I used the emulator tab inside another project in a stable Android Studio instead... I eventually gave in and reverted to using an emulator in a separate window.&lt;/li&gt;
&lt;li&gt;Sometimes Studio just froze up entirely, and its process had to be killed and restarted. This happened about once a day or so.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I heard some similar anecdotal stories from others, including excessive resource usage when using Compose compared to other projects. These were occasionally fixable by restarting, invalidating caches, restarting adb, and similar "usual" debugging practices.&lt;/p&gt;




&lt;p&gt;Another pain point of using Jetpack Compose can be that looking up how to do things is a bit hit-and-miss for the time being. For example, when you land on StackOverflow questions about it, you'll often find answers that say "this is how to do it in dev11", or "here's the new API for this in alpha3", but then none of those APIs will be present in the current beta version.&lt;/p&gt;

&lt;p&gt;Of course, on one hand it's nice that Compose went through a lot of iteration and they weren't afraid to change and evolve APIs - that's what the preview versions are for. &lt;/p&gt;

&lt;p&gt;On the other, it makes for a somewhat unpleasant experience for developers trying to build things on the latest Compose version. Thankfully this situation will naturally improve over time, but we'll have to wait a bit to get enough new, up-to-date answers in.&lt;/p&gt;

&lt;h3&gt;
  
  
  The resources
&lt;/h3&gt;

&lt;p&gt;Since StackOverflow won't be your best friend for learning Compose yet, where else can you go?&lt;/p&gt;

&lt;p&gt;First, there's Google's official resources, which are a decent start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Their best resource by far is the written documentation pages - &lt;a href="https://developer.android.com/jetpack/compose/documentation" rel="noopener noreferrer"&gt;get started with them here&lt;/a&gt;. These include a lot of helpful explanation and snippets for designing things in Compose, and also touch on more advanced topics like navigation, architecture, and interop with existing code.&lt;/li&gt;
&lt;li&gt;There's a lot of video content about Compose as well, under the #AndroidShow branding. I'll recommend skipping most of this, especially the livestream and Q&amp;amp;A parts - they're not really developer-focused, and contain a lot of hype and marketing.
What contains a lot of value, however, are their screencasts about Compose features (which are also included in sections of the written docs). You can find these &lt;a href="https://www.youtube.com/playlist?list=PLWz5rJ2EKKc8fZY3smX9CPx9Y_O80ycAd" rel="noopener noreferrer"&gt;in this YouTube playlist&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;If you're new to Compose, the &lt;a href="https://www.youtube.com/watch?v=Ef1xKWjA9E8" rel="noopener noreferrer"&gt;Beta overview video&lt;/a&gt; is also worth watching. For more longer-form introductory content, these videos from last year are all really helpful:  &lt;a href="https://www.youtube.com/watch?v=U5BwfqBpiWU&amp;amp;" rel="noopener noreferrer"&gt;Jetpack Compose&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=SMOhl9RK0BA" rel="noopener noreferrer"&gt;Thinking in Compose&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=DDd6IOlH3io" rel="noopener noreferrer"&gt;Compose by example&lt;/a&gt;, and &lt;a href="https://www.youtube.com/watch?v=PjQdFmiDgwk" rel="noopener noreferrer"&gt;Compose for existing apps&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;For hands-on practice, take a look at Google's &lt;a href="https://developer.android.com/courses/pathways/compose" rel="noopener noreferrer"&gt;Compose Pathway&lt;/a&gt; that includes videos, articles, and code labs. This goes into significant detail in some parts - feel free to skip the things you're not interested in learning deeply immediately, otherwise it'll take a long time to get through it.

&lt;ul&gt;
&lt;li&gt;I also found that some of the codelabs here were outdated (not using beta1 APIs yet), so be prepared to fiddle a bit with the code snippets provided, or to debug dependencies. Again, this is something that will be improved soon.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;If you prefer reading, &lt;a href="https://www.raywenderlich.com/books/jetpack-compose-by-tutorials/v1.0" rel="noopener noreferrer"&gt;RayWenderlich team's Jetpack Compose book&lt;/a&gt; has really solid, professional content in it, providing a complete tour for you to get started with: A nice exploration of the basic UI building blocks of Compose, as well as multiple example apps. (This, again, is slightly outdated, as its samples use alpha10. Fingers crossed that it gets a revision soon.)&lt;/p&gt;

&lt;p&gt;For even more long-form video explanations, guidance, and demos, &lt;a href="https://www.youtube.com/watch?v=2n2mtNfg25s" rel="noopener noreferrer"&gt;Exploring Jetpack Compose&lt;/a&gt; and &lt;a href="https://www.youtube.com/watch?v=eL3G1Basx1o" rel="noopener noreferrer"&gt;Advanced Jetpack Compose&lt;/a&gt; by Filip Babić are both excellent.&lt;/p&gt;

&lt;p&gt;To find quick snippets for how to perform basic things, the &lt;a href="https://compose.academy/" rel="noopener noreferrer"&gt;Compose Academy site&lt;/a&gt; can come in handy.&lt;/p&gt;

&lt;p&gt;Finally, exploring Compose on your own is also a great way to learn about it. All of the APIs you use are just Kotlin code, so exploring through code completion, and jumping into the underlying implementation in Android Studio to read the source are super easy. The in-code documentation for most Compose APIs also tends to be excellent. &lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Are you excited and ready for Compose? We know that we are, and you can look forward to more Compose content from us soon, including in our &lt;a href="https://getstream.io/chat/sdk/android/" rel="noopener noreferrer"&gt;Android Chat SDK&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Tweet at us &lt;a href="https://twitter.com/getstream_io" rel="noopener noreferrer"&gt;@getstream_io&lt;/a&gt; and tell us what you think about Compose, and what you found to be the most useful way to learn about it!&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://getstream.io/blog/jetpack-compose-impressions-resources/" rel="noopener noreferrer"&gt;getstream.io/blog&lt;/a&gt;.&lt;/p&gt;

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