<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.3">Jekyll</generator><link href="https://johnoreilly.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://johnoreilly.dev/" rel="alternate" type="text/html" /><updated>2025-12-22T15:19:45+00:00</updated><id>https://johnoreilly.dev/feed.xml</id><title type="html">John O’Reilly</title><subtitle>John O&apos;Reilly&apos;s Blog</subtitle><entry><title type="html">Using Navigation 3 with Compose Multiplatform</title><link href="https://johnoreilly.dev/posts/navigation3-cmp/" rel="alternate" type="text/html" title="Using Navigation 3 with Compose Multiplatform" /><published>2025-11-15T00:00:00+00:00</published><updated>2025-11-15T00:00:00+00:00</updated><id>https://johnoreilly.dev/posts/navigation3-cmp</id><content type="html" xml:base="https://johnoreilly.dev/posts/navigation3-cmp/"><![CDATA[<p><a href="https://developer.android.com/guide/navigation/navigation-3">Navigation 3</a> is a new Compose based navigation library from Google that’s designed with ease of use and flexibility in mind.</p>

<blockquote>
  <p>With Navigation 3, you have full control over your back stack, and navigating to and from destinations is as simple as adding and removing items from a list.</p>
</blockquote>

<p>As with a number of the other Jetpack libraries, JetBrains maintain their own fork that works with Compose Multiplatform.  In this article we’re going to outline how we can use Navigation 3 in the <a href="https://github.com/joreilly/FantasyPremierLeague">FantasyPremierLeague</a> Compose Multiplatform sample. Note that all the code shown here is defined in <code class="language-plaintext highlighter-rouge">commonMain</code> in the project’s shared KMP code.</p>

<h3 id="implementation">Implementation</h3>

<p>We firstly add the following Navigation 3 dependencies (using the JetBrains versions of the library).  As well as the core ui dependency we’re also including the Material3 Adaptive Navigation3 library (we’ll show later in the article how that can be used to implement adaptive layouts).</p>

<h6 id="libsversiontoml">libs.version.toml</h6>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="code"><pre><span class="n">androidxNavigation3UI</span> <span class="p">=</span> <span class="s">"1.0.0-alpha04"</span>
<span class="n">androidxNavigation3Material</span> <span class="p">=</span> <span class="s">"1.3.0-alpha01"</span>


<span class="n">androidx-navigation3-ui</span> <span class="p">=</span> <span class="p">{</span> <span class="n">module</span> <span class="p">=</span> <span class="s">"org.jetbrains.androidx.navigation3:navigation3-ui"</span><span class="p">,</span> <span class="n">version</span><span class="p">.</span><span class="n">ref</span> <span class="p">=</span> <span class="s">"androidxNavigation3UI"</span> <span class="p">}</span>
<span class="n">androidx-navigation3-material3-adaptive</span> <span class="p">=</span> <span class="p">{</span> <span class="n">module</span> <span class="p">=</span> <span class="s">"org.jetbrains.compose.material3.adaptive:adaptive-navigation3"</span><span class="p">,</span> <span class="n">version</span><span class="p">.</span><span class="n">ref</span> <span class="p">=</span> <span class="s">"androidxNavigation3Material"</span> <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h6 id="buildgradlekts">build.gradle.kts</h6>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
</pre></td><td class="code"><pre><span class="nf">implementation</span><span class="p">(</span><span class="n">libs</span><span class="p">.</span><span class="n">androidx</span><span class="p">.</span><span class="n">navigation3</span><span class="p">.</span><span class="n">ui</span><span class="p">)</span>
<span class="nf">implementation</span><span class="p">(</span><span class="n">libs</span><span class="p">.</span><span class="n">androidx</span><span class="p">.</span><span class="n">navigation3</span><span class="p">.</span><span class="n">material3</span><span class="p">.</span><span class="n">adaptive</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Next up we define the following keys/routes which will be used to map to particular content in the app. We’re also defining “top level” routes here which we’ll use in our <code class="language-plaintext highlighter-rouge">NavigationBar</code> implementation.</p>

<p>Note that we’re using <code class="language-plaintext highlighter-rouge">@Serializable</code> here to allow making the back stack persistent.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
</pre></td><td class="code"><pre><span class="nd">@Serializable</span>
<span class="k">private</span> <span class="k">sealed</span> <span class="kd">interface</span> <span class="nc">Route</span>

<span class="nd">@Serializable</span>
<span class="k">private</span> <span class="k">sealed</span> <span class="kd">interface</span> <span class="nc">TopLevelRoute</span><span class="p">:</span> <span class="nc">Route</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">icon</span><span class="p">:</span> <span class="nc">ImageVector</span>
    <span class="kd">val</span> <span class="py">contentDescription</span><span class="p">:</span> <span class="nc">String</span>
<span class="p">}</span>

<span class="nd">@Serializable</span>
<span class="k">private</span> <span class="n">data</span> <span class="kd">object</span> <span class="nc">PlayerList</span> <span class="p">:</span> <span class="nc">TopLevelRoute</span> <span class="p">{</span>
    <span class="k">override</span> <span class="kd">val</span> <span class="py">icon</span> <span class="p">=</span> <span class="nc">Icons</span><span class="p">.</span><span class="nc">Default</span><span class="p">.</span><span class="nc">Person</span>
    <span class="k">override</span> <span class="kd">val</span> <span class="py">contentDescription</span> <span class="p">=</span> <span class="s">"Players"</span>
<span class="p">}</span>

<span class="nd">@Serializable</span>
<span class="k">private</span> <span class="kd">data class</span> <span class="nc">PlayerDetails</span><span class="p">(</span><span class="kd">val</span> <span class="py">playerId</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Route</span>

<span class="nd">@Serializable</span>
<span class="k">private</span> <span class="n">data</span> <span class="kd">object</span> <span class="nc">FixtureList</span> <span class="p">:</span> <span class="nc">TopLevelRoute</span> <span class="p">{</span>
    <span class="k">override</span> <span class="kd">val</span> <span class="py">icon</span> <span class="p">=</span> <span class="nc">Icons</span><span class="p">.</span><span class="nc">Filled</span><span class="p">.</span><span class="nc">DateRange</span>
    <span class="k">override</span> <span class="kd">val</span> <span class="py">contentDescription</span> <span class="p">=</span> <span class="s">"Fixtures"</span>
<span class="p">}</span>

<span class="nd">@Serializable</span>
<span class="k">private</span> <span class="n">data</span> <span class="kd">object</span> <span class="nc">League</span> <span class="p">:</span> <span class="nc">TopLevelRoute</span> <span class="p">{</span>
    <span class="k">override</span> <span class="kd">val</span> <span class="py">icon</span> <span class="p">=</span> <span class="nc">Icons</span><span class="p">.</span><span class="nc">AutoMirrored</span><span class="p">.</span><span class="nc">Filled</span><span class="p">.</span><span class="nc">List</span>
    <span class="k">override</span> <span class="kd">val</span> <span class="py">contentDescription</span> <span class="p">=</span> <span class="s">"Leagues"</span>
<span class="p">}</span>

<span class="nd">@Serializable</span>
<span class="k">private</span> <span class="n">data</span> <span class="kd">object</span> <span class="nc">Settings</span> <span class="p">:</span> <span class="nc">Route</span>

<span class="k">private</span> <span class="kd">val</span> <span class="py">topLevelRoutes</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">TopLevelRoute</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">listOf</span><span class="p">(</span><span class="nc">PlayerList</span><span class="p">,</span> <span class="nc">FixtureList</span><span class="p">,</span> <span class="nc">League</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>We then create our back stack and use <code class="language-plaintext highlighter-rouge">NavDisplay</code> to manage our app’s key/content mapping (along with navigating to and from destinations by adding and removing items from that back stack).</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
</pre></td><td class="code"><pre><span class="kd">val</span> <span class="py">backStack</span><span class="p">:</span> <span class="nc">MutableList</span><span class="p">&lt;</span><span class="nc">Route</span><span class="p">&gt;</span> <span class="p">=</span>
    <span class="nf">rememberSerializable</span><span class="p">(</span><span class="n">serializer</span> <span class="p">=</span> <span class="nc">SnapshotStateListSerializer</span><span class="p">())</span> <span class="p">{</span>
        <span class="nf">mutableStateListOf</span><span class="p">(</span><span class="nc">PlayerList</span><span class="p">)</span>
    <span class="p">}</span>

<span class="nc">Scaffold</span><span class="p">(</span>
    <span class="n">bottomBar</span> <span class="p">=</span> <span class="p">{</span> <span class="nc">FantasyPremierLeagueBottomNavigation</span><span class="p">(</span><span class="n">topLevelRoutes</span><span class="p">,</span> <span class="n">backStack</span><span class="p">)</span> <span class="p">}</span>
<span class="p">){</span>
    <span class="nc">NavDisplay</span><span class="p">(</span>
        <span class="n">backStack</span> <span class="p">=</span> <span class="n">backStack</span><span class="p">,</span>
        <span class="n">onBack</span> <span class="p">=</span> <span class="p">{</span> <span class="n">backStack</span><span class="p">.</span><span class="nf">removeLastOrNull</span><span class="p">()</span> <span class="p">},</span>
        <span class="n">entryProvider</span> <span class="p">=</span> <span class="nf">entryProvider</span> <span class="p">{</span>
            <span class="n">entry</span><span class="p">&lt;</span><span class="nc">PlayerList</span><span class="p">&gt;</span> <span class="p">{</span>
                <span class="nc">PlayerListView</span><span class="p">(</span>
                    <span class="n">onPlayerSelected</span> <span class="p">=</span> <span class="p">{</span> <span class="n">player</span> <span class="p">-&gt;</span>
                        <span class="n">backStack</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">PlayerDetails</span><span class="p">(</span><span class="n">player</span><span class="p">.</span><span class="n">id</span><span class="p">))</span>
                    <span class="p">},</span>
                    <span class="n">onShowSettings</span> <span class="p">=</span> <span class="p">{</span>
                        <span class="n">backStack</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">Settings</span><span class="p">)</span>
                    <span class="p">}</span>
                <span class="p">)</span>
            <span class="p">}</span>
            <span class="n">entry</span><span class="p">&lt;</span><span class="nc">PlayerDetails</span><span class="p">&gt;</span> <span class="p">{</span> <span class="n">key</span> <span class="p">-&gt;</span>
                <span class="kd">val</span> <span class="py">viewModel</span> <span class="p">=</span> <span class="n">koinViewModel</span><span class="p">&lt;</span><span class="nc">PlayerDetailsViewModel</span><span class="p">&gt;()</span>
                <span class="n">viewModel</span><span class="p">.</span><span class="nf">setPlayer</span><span class="p">(</span><span class="n">key</span><span class="p">.</span><span class="n">playerId</span><span class="p">)</span>
                <span class="nc">PlayerDetailsView</span><span class="p">(</span><span class="n">viewModel</span><span class="p">,</span> <span class="n">popBackStack</span> <span class="p">=</span> <span class="p">{</span> <span class="n">backStack</span><span class="p">.</span><span class="nf">removeLastOrNull</span><span class="p">()</span> <span class="p">})</span>
            <span class="p">}</span>
            <span class="n">entry</span><span class="p">&lt;</span><span class="nc">FixtureList</span><span class="p">&gt;</span> <span class="p">{</span> <span class="nc">FixturesListView</span><span class="p">()</span> <span class="p">}</span>
            <span class="n">entry</span><span class="p">&lt;</span><span class="nc">League</span><span class="p">&gt;</span> <span class="p">{</span> <span class="nc">LeagueListView</span><span class="p">()</span> <span class="p">}</span>
            <span class="n">entry</span><span class="p">&lt;</span><span class="nc">Settings</span><span class="p">&gt;</span> <span class="p">{</span> <span class="nc">SettingsView</span> <span class="p">{</span> <span class="n">popBackStack</span> <span class="p">=</span> <span class="n">backStack</span><span class="p">.</span><span class="nf">removeLastOrNull</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>And finally this is how we setup our <code class="language-plaintext highlighter-rouge">NavigationBar</code> to use those top level routes mentioned earlier (again we navigate to particular content by just adding entries to our back stack).</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
</pre></td><td class="code"><pre><span class="nd">@Composable</span>
<span class="k">private</span> <span class="k">fun</span> <span class="nf">FantasyPremierLeagueBottomNavigation</span><span class="p">(</span>
    <span class="n">topLevelRoutes</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">TopLevelRoute</span><span class="p">&gt;,</span>
    <span class="n">backStack</span><span class="p">:</span> <span class="nc">MutableList</span><span class="p">&lt;</span><span class="nc">Route</span><span class="p">&gt;</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="py">selectedType</span> <span class="k">by</span> <span class="nf">remember</span> <span class="p">{</span> <span class="n">mutableStateOf</span><span class="p">&lt;</span><span class="nc">TopLevelRoute</span><span class="p">&gt;(</span><span class="nc">PlayerList</span><span class="p">)</span> <span class="p">}</span>
    <span class="nc">NavigationBar</span> <span class="p">{</span>
        <span class="n">topLevelRoutes</span><span class="p">.</span><span class="nf">forEach</span> <span class="p">{</span> <span class="n">topLevelRoute</span> <span class="p">-&gt;</span>
            <span class="nc">NavigationBarItem</span><span class="p">(</span>
                <span class="n">icon</span> <span class="p">=</span> <span class="p">{</span>
                    <span class="nc">Icon</span><span class="p">(</span>
                        <span class="n">imageVector</span> <span class="p">=</span> <span class="n">topLevelRoute</span><span class="p">.</span><span class="n">icon</span><span class="p">,</span>
                        <span class="n">contentDescription</span> <span class="p">=</span> <span class="n">topLevelRoute</span><span class="p">.</span><span class="n">contentDescription</span>
                    <span class="p">)</span>
                <span class="p">},</span>
                <span class="n">selected</span> <span class="p">=</span> <span class="n">topLevelRoute</span> <span class="p">==</span> <span class="n">selectedType</span><span class="p">,</span>
                <span class="n">onClick</span> <span class="p">=</span> <span class="p">{</span>
                    <span class="n">selectedType</span> <span class="p">=</span> <span class="n">topLevelRoute</span>
                    <span class="n">backStack</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">topLevelRoute</span><span class="p">)</span>
                <span class="p">}</span>
            <span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>

<h3 id="adaptive-layout">Adaptive Layout</h3>

<p>As mentioned earlier we can also support adaptive layouts using the Material3 Adaptive Navigation3 library.  In our example we do this by firstly creating the appropriate <code class="language-plaintext highlighter-rouge">SceneStrategy</code> (<code class="language-plaintext highlighter-rouge">ListDetailSceneStrategy</code> in our case).</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="code"><pre><span class="kd">val</span> <span class="py">windowAdaptiveInfo</span> <span class="p">=</span> <span class="nf">currentWindowAdaptiveInfo</span><span class="p">()</span>
<span class="kd">val</span> <span class="py">directive</span> <span class="p">=</span> <span class="nf">remember</span><span class="p">(</span><span class="n">windowAdaptiveInfo</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">calculatePaneScaffoldDirective</span><span class="p">(</span><span class="n">windowAdaptiveInfo</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">copy</span><span class="p">(</span><span class="n">horizontalPartitionSpacerSize</span> <span class="p">=</span> <span class="mi">0</span><span class="p">.</span><span class="n">dp</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">val</span> <span class="py">listDetailStrategy</span> <span class="p">=</span> <span class="n">rememberListDetailSceneStrategy</span><span class="p">&lt;</span><span class="nc">Any</span><span class="p">&gt;(</span><span class="n">directive</span> <span class="p">=</span> <span class="n">directive</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>We then pass that strategy to <code class="language-plaintext highlighter-rouge">NavDisplay</code> along with now using the appropriate <code class="language-plaintext highlighter-rouge">metadata</code> values as shown below.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
</pre></td><td class="code"><pre><span class="nc">NavDisplay</span><span class="p">(</span>
    <span class="n">backStack</span> <span class="p">=</span> <span class="n">backStack</span><span class="p">,</span>
    <span class="n">onBack</span> <span class="p">=</span> <span class="p">{</span> <span class="n">backStack</span><span class="p">.</span><span class="nf">removeLastOrNull</span><span class="p">()</span> <span class="p">},</span>
    <span class="n">sceneStrategy</span> <span class="p">=</span> <span class="n">listDetailStrategy</span><span class="p">,</span>
    <span class="n">entryProvider</span> <span class="p">=</span> <span class="nf">entryProvider</span> <span class="p">{</span>
        <span class="n">entry</span><span class="p">&lt;</span><span class="nc">PlayerList</span><span class="p">&gt;(</span>
            <span class="n">metadata</span> <span class="p">=</span> <span class="nc">ListDetailSceneStrategy</span><span class="p">.</span><span class="nf">listPane</span><span class="p">(</span>
                <span class="n">detailPlaceholder</span> <span class="p">=</span> <span class="p">{</span>
                    <span class="nc">Box</span><span class="p">(</span>
                        <span class="n">modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">fillMaxSize</span><span class="p">(),</span>
                        <span class="n">contentAlignment</span> <span class="p">=</span> <span class="nc">Alignment</span><span class="p">.</span><span class="nc">Center</span>
                    <span class="p">)</span> <span class="p">{</span>
                        <span class="nc">Text</span><span class="p">(</span>
                            <span class="n">text</span> <span class="p">=</span> <span class="s">"Choose a player from the list"</span><span class="p">,</span>
                            <span class="n">textAlign</span> <span class="p">=</span> <span class="nc">TextAlign</span><span class="p">.</span><span class="nc">Center</span>
                        <span class="p">)</span>
                    <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">)</span>
        <span class="p">)</span> <span class="p">{</span>
            <span class="nc">PlayerListView</span><span class="p">(</span>
                <span class="n">onPlayerSelected</span> <span class="p">=</span> <span class="p">{</span> <span class="n">player</span> <span class="p">-&gt;</span>
                    <span class="n">backStack</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">PlayerDetails</span><span class="p">(</span><span class="n">player</span><span class="p">.</span><span class="n">id</span><span class="p">))</span>
                <span class="p">},</span>
                <span class="n">onShowSettings</span> <span class="p">=</span> <span class="p">{</span> <span class="n">backStack</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">Settings</span><span class="p">)</span> <span class="p">}</span>
            <span class="p">)</span>
        <span class="p">}</span>
        <span class="n">entry</span><span class="p">&lt;</span><span class="nc">PlayerDetails</span><span class="p">&gt;(</span>
            <span class="n">metadata</span> <span class="p">=</span> <span class="nc">ListDetailSceneStrategy</span><span class="p">.</span><span class="nf">detailPane</span><span class="p">()</span>
        <span class="p">)</span> <span class="p">{</span> <span class="n">key</span> <span class="p">-&gt;</span>
            <span class="kd">val</span> <span class="py">viewModel</span> <span class="p">=</span> <span class="n">koinViewModel</span><span class="p">&lt;</span><span class="nc">PlayerDetailsViewModel</span><span class="p">&gt;()</span>
            <span class="n">viewModel</span><span class="p">.</span><span class="nf">setPlayer</span><span class="p">(</span><span class="n">key</span><span class="p">.</span><span class="n">playerId</span><span class="p">)</span>
            <span class="nc">PlayerDetailsView</span><span class="p">(</span>
                <span class="n">viewModel</span><span class="p">,</span>
                <span class="n">popBackStack</span> <span class="p">=</span> <span class="p">{</span> <span class="n">backStack</span><span class="p">.</span><span class="nf">removeLastOrNull</span><span class="p">()</span> <span class="p">})</span>
        <span class="p">}</span>
        <span class="n">entry</span><span class="p">&lt;</span><span class="nc">FixtureList</span><span class="p">&gt;</span> <span class="p">{</span> <span class="nc">FixturesListView</span><span class="p">()</span> <span class="p">}</span>
        <span class="n">entry</span><span class="p">&lt;</span><span class="nc">League</span><span class="p">&gt;</span> <span class="p">{</span> <span class="nc">LeagueListView</span><span class="p">()</span> <span class="p">}</span>
        <span class="n">entry</span><span class="p">&lt;</span><span class="nc">Settings</span><span class="p">&gt;</span> <span class="p">{</span> <span class="nc">SettingsView</span> <span class="p">{</span> <span class="n">popBackStack</span> <span class="p">=</span> <span class="n">backStack</span><span class="p">.</span><span class="nf">removeLastOrNull</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span>
    <span class="p">},</span>
<span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>With these changes we now have an adaptive list/detail layout that adjusts accordingly as the window size changes (this screenshot is from the Compose for Desktop client but works in exactly the same way in the Android and iOS clients).</p>

<p><img src="/images/fpl_nav_screenshot.png" alt="FPL" /></p>

<p>Note that several other navigation use cases are covered in the excellent <a href="https://github.com/android/nav3-recipes">Navigation 3 - Code recipes</a> repository.</p>

<p><br />
Featured in <a href="https://mailchi.mp/kotlinweekly/kotlin-weekly-486">Kotlin Weekly Issue #486</a>, <a href="https://androidweekly.net/issues/issue-702">Android Weekly #702</a> and <a href="https://jetc.dev/issues/291.html">jetc.dev Newletter Issue #291</a></p>

<h3 id="related-tweet">Related tweet</h3>

<div class="jekyll-twitter-plugin"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Using Navigation 3 with Compose Multiplatform <a href="https://twitter.com/hashtag/KMP?src=hash&amp;ref_src=twsrc%5Etfw">#KMP</a> <a href="https://twitter.com/hashtag/CMP?src=hash&amp;ref_src=twsrc%5Etfw">#CMP</a> <a href="https://t.co/aeUR018DkU">https://t.co/aeUR018DkU</a></p>&mdash; John O&#39;Reilly (@joreilly) <a href="https://twitter.com/joreilly/status/1989848215504781562?ref_src=twsrc%5Etfw">November 16, 2025</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</div>]]></content><author><name></name></author><summary type="html"><![CDATA[Navigation 3 is a new Compose based navigation library from Google that’s designed with ease of use and flexibility in mind.]]></summary></entry><entry><title type="html">Deploying a Kotlin-based remote MCP Server to Google Cloud Run</title><link href="https://johnoreilly.dev/posts/remote-mcp/" rel="alternate" type="text/html" title="Deploying a Kotlin-based remote MCP Server to Google Cloud Run" /><published>2025-07-27T00:00:00+01:00</published><updated>2025-07-27T00:00:00+01:00</updated><id>https://johnoreilly.dev/posts/remote-mcp</id><content type="html" xml:base="https://johnoreilly.dev/posts/remote-mcp/"><![CDATA[<p>I wrote <a href="https://johnoreilly.dev/posts/kotlin-mcp-kmp/">a previous post</a> about how to develop an MCP Server using the <a href="https://github.com/modelcontextprotocol/kotlin-sdk">Kotlin MCP SDK</a>.  That was  primarily based on local deployment (using stdio protocol)…a setup that Claude Desktop for example could only support at the time.  With the announcement that Claude now supports access to remote MCP Servers (from desktop and mobile) I thought I’d take a look at deploying the MCP Server in the <a href="https://github.com/joreilly/ClimateTraceKMP">ClimateTraceKMP</a> sample to Google Cloud Run.</p>

<div class="jekyll-twitter-plugin"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Your connected tools are now available in Claude on your mobile device. <br /><br />Now you can access projects, create new docs, and complete work while on the go. <a href="https://t.co/tqWuQ5r6Gc">pic.twitter.com/tqWuQ5r6Gc</a></p>&mdash; Anthropic (@AnthropicAI) <a href="https://twitter.com/AnthropicAI/status/1948784311265894447?ref_src=twsrc%5Etfw">July 25, 2025</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</div>

<p>The first thing we need to do is create a Google Cloud project that we can deploy our server to (using Cloud Run)….something we can do for example using the <a href="https://console.cloud.google.com/cloud-resource-manager">Cloud Resource Manager</a>.  In our case we created <code class="language-plaintext highlighter-rouge">climatetrace-mcp</code> and we set that as the active gcloud project as follows.</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">gcloud config <span class="nb">set </span>project climatetrace-mcp</code></pre></figure>

<p>Before we can run our server on Cloud Run we need to build the code, package as a container and deploy that container to <a href="https://console.cloud.google.com/artifacts">Artifact Registry</a>.  We do that using the <a href="https://github.com/GoogleContainerTools/jib">Jib Gradle Plugin</a> as shown below.</p>

<p><strong>libs.version.toml</strong></p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="code"><pre><span class="na">[versions]</span>
<span class="o">..</span><span class="p">.</span>
<span class="n">jib</span> <span class="p">=</span> <span class="s">"3.4.5"</span>
<span class="na">

[plugins]</span>
<span class="o">..</span><span class="p">.</span>
<span class="n">jib</span> <span class="p">=</span> <span class="p">{</span> <span class="n">id</span> <span class="p">=</span> <span class="s">"com.google.cloud.tools.jib"</span><span class="p">,</span> <span class="n">version</span><span class="p">.</span><span class="n">ref</span> <span class="p">=</span> <span class="s">"jib"</span> <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><strong>build.gradle.kts</strong></p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="code"><pre><span class="nf">plugins</span> <span class="p">{</span>
    <span class="o">..</span><span class="p">.</span>
    <span class="nf">alias</span><span class="p">(</span><span class="n">libs</span><span class="p">.</span><span class="n">plugins</span><span class="p">.</span><span class="n">jib</span><span class="p">)</span>
<span class="p">}</span>

<span class="o">..</span><span class="p">.</span>

<span class="nf">jib</span> <span class="p">{</span>
    <span class="n">from</span><span class="p">.</span><span class="n">image</span> <span class="p">=</span> <span class="s">"docker.io/library/eclipse-temurin:21"</span>

    <span class="nf">to</span> <span class="p">{</span>
        <span class="n">image</span> <span class="p">=</span> <span class="s">"gcr.io/climatetrace-mcp/climatetrace-mcp-server"</span>
    <span class="p">}</span>
    <span class="nf">container</span> <span class="p">{</span>
        <span class="n">ports</span> <span class="p">=</span> <span class="nf">listOf</span><span class="p">(</span><span class="s">"8080"</span><span class="p">)</span>
        <span class="n">mainClass</span> <span class="p">=</span> <span class="s">"McpServerKt"</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>We run the <code class="language-plaintext highlighter-rouge">jib</code> gradle task then which will create and deploy the container image.</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">./gradlew :mcp-server:jib</code></pre></figure>

<p>Note the following is what the container is configured to run (this makes use of the <code class="language-plaintext highlighter-rouge">mcp</code> Ktor extension function from the Kotlin MCP SDK which sets up the required SSE support).</p>

<p><strong>McpServer.kt</strong></p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">server</span> <span class="p">=</span> <span class="nf">configureMcpServer</span><span class="p">()</span>

    <span class="kd">val</span> <span class="py">port</span> <span class="p">=</span> <span class="nc">System</span><span class="p">.</span><span class="nf">getenv</span><span class="p">().</span><span class="nf">getOrDefault</span><span class="p">(</span><span class="s">"PORT"</span><span class="p">,</span> <span class="s">"8080"</span><span class="p">).</span><span class="nf">toInt</span><span class="p">()</span>
    <span class="nf">embeddedServer</span><span class="p">(</span><span class="nc">CIO</span><span class="p">,</span> <span class="n">port</span><span class="p">,</span> <span class="n">host</span> <span class="p">=</span> <span class="s">"0.0.0.0"</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">mcp</span> <span class="p">{</span>
            <span class="n">server</span>
        <span class="p">}</span>
    <span class="p">}.</span><span class="nf">start</span><span class="p">(</span><span class="n">wait</span> <span class="p">=</span> <span class="k">true</span><span class="p">)</span>
<span class="p">}</span></code></pre></figure>

<p><br /></p>

<p>And finally we can deploy that container to Cloud Run using the following command.</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">gcloud run deploy climatetrace-mcp <span class="nt">--image</span><span class="o">=</span>gcr.io/climatetrace-mcp/climatetrace-mcp-server</code></pre></figure>

<p>We can now configure Claude Desktop for example to use our deployed MCP Server.
<img src="/images/claude_desktop_remote_mcp.png" alt="Claude Deskop" /></p>

<p>And now, with that server configured, we can also access from the Claude mobile apps.</p>

<p><img src="/images/claude_mobile.jpeg" alt="Claude Mobile" /></p>

<p>We can also interact with the server using <a href="https://modelcontextprotocol.io/legacy/tools/inspector">MCP Inspector</a>.
<img src="/images/mcp_inspector.png" alt="MCP Inspector" /></p>

<p>The code shown here is part of the <code class="language-plaintext highlighter-rouge">mcp-server</code> module in the <a href="https://github.com/joreilly/ClimateTraceKMP">ClimateTraceKMP</a> repository.</p>

<p><br />
Featured in <a href="https://mailchi.mp/kotlinweekly/kotlin-weekly-470">Kotlin Weekly Issue #470</a> and <a href="https://androidweekly.net/issues/issue-686">Android Weekly #686</a></p>

<h3 id="related-tweet">Related tweet</h3>

<div class="jekyll-twitter-plugin"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Deploying a Kotlin-based remote MCP Server to Google Cloud Run <a href="https://t.co/IpgvQL4gdl">https://t.co/IpgvQL4gdl</a><br /><br />(I finally succumbed to creating an AI generated image to represent the post...using Gemini in this case 😀)</p>&mdash; John O&#39;Reilly (@joreilly) <a href="https://twitter.com/joreilly/status/1949585189258023409?ref_src=twsrc%5Etfw">July 27, 2025</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</div>]]></content><author><name></name></author><summary type="html"><![CDATA[I wrote a previous post about how to develop an MCP Server using the Kotlin MCP SDK. That was primarily based on local deployment (using stdio protocol)…a setup that Claude Desktop for example could only support at the time. With the announcement that Claude now supports access to remote MCP Servers (from desktop and mobile) I thought I’d take a look at deploying the MCP Server in the ClimateTraceKMP sample to Google Cloud Run.]]></summary></entry><entry><title type="html">Using Google’s Agent Development Kit for Java from Kotlin code</title><link href="https://johnoreilly.dev/posts/kotlin-adk/" rel="alternate" type="text/html" title="Using Google’s Agent Development Kit for Java from Kotlin code" /><published>2025-07-11T00:00:00+01:00</published><updated>2025-07-11T00:00:00+01:00</updated><id>https://johnoreilly.dev/posts/kotlin-adk</id><content type="html" xml:base="https://johnoreilly.dev/posts/kotlin-adk/"><![CDATA[<p>Google’s <a href="https://github.com/google/adk-java">Agent Development Kit for Java</a> is described as “an open-source, code-first Java toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control”.  In this article we’re going to see how it can be consumed in Kotlin code (specifically in the <code class="language-plaintext highlighter-rouge">agents</code> module in the <a href="https://github.com/joreilly/ClimateTraceKMP">ClimateTrace</a> Kotlin Multiplatform sample).</p>

<p>The first thing we need to do is add the following gradle dependencies to our project (<code class="language-plaintext highlighter-rouge">0.2.0</code> being the latest version at time of writing this).</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
</pre></td><td class="code"><pre><span class="nf">implementation</span><span class="p">(</span><span class="s">"com.google.adk:google-adk:0.2.0"</span><span class="p">)</span>
<span class="nf">implementation</span><span class="p">(</span><span class="s">"com.google.adk:google-adk-dev:0.2.0"</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>ADK works with the following LLM providers and shown below is an example of creating a Gemini based model.</p>
<ul>
  <li>Google</li>
  <li>OpenAI</li>
  <li>Anthropic</li>
  <li>OpenRouter</li>
  <li>Ollama</li>
</ul>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="code"><pre><span class="kd">val</span> <span class="py">model</span> <span class="p">=</span> <span class="nc">Gemini</span><span class="p">(</span>
    <span class="s">"gemini-1.5-pro"</span><span class="p">,</span>
    <span class="nc">Client</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">apiKey</span><span class="p">(</span><span class="n">apiKeyGoogle</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
<span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>With the model created we can now create our AI agent.  We’re also providing tools to the agent….more about that in a later section.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="code"><pre><span class="kd">val</span> <span class="py">agent</span> <span class="p">=</span> <span class="nc">LlmAgent</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">name</span><span class="p">(</span><span class="nc">NAME</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">model</span><span class="p">(</span><span class="n">model</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">description</span><span class="p">(</span><span class="s">"Agent to answer climate emissions related questions."</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">instruction</span><span class="p">(</span><span class="s">"You are an agent that provides climate emissions related information. Use 3 letter country codes."</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">tools</span><span class="p">(</span><span class="n">tools</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>The following then shows how we can run the agent with the given prompt.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
</pre></td><td class="code"><pre><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">runner</span> <span class="p">=</span> <span class="nc">InMemoryRunner</span><span class="p">(</span><span class="nf">initAgent</span><span class="p">())</span>
    <span class="kd">val</span> <span class="py">session</span> <span class="p">=</span> <span class="n">runner</span>
        <span class="p">.</span><span class="nf">sessionService</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">createSession</span><span class="p">(</span><span class="nc">NAME</span><span class="p">,</span> <span class="nc">USER_ID</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">blockingGet</span><span class="p">()</span>

    <span class="kd">val</span> <span class="py">prompt</span> <span class="p">=</span>
        <span class="s">"""
            Get emission data for EU countries in 2024.
            Use units of millions for the emissions data.
            Show result in a grid of decreasing order of emissions.
            """</span><span class="p">.</span><span class="nf">trimIndent</span><span class="p">()</span>

    <span class="kd">val</span> <span class="py">userMsg</span> <span class="p">=</span> <span class="nc">Content</span><span class="p">.</span><span class="nf">fromParts</span><span class="p">(</span><span class="nc">Part</span><span class="p">.</span><span class="nf">fromText</span><span class="p">(</span><span class="n">prompt</span><span class="p">))</span>
    <span class="kd">val</span> <span class="py">events</span> <span class="p">=</span> <span class="n">runner</span><span class="p">.</span><span class="nf">runAsync</span><span class="p">(</span><span class="nc">USER_ID</span><span class="p">,</span> <span class="n">session</span><span class="p">.</span><span class="nf">id</span><span class="p">(),</span> <span class="n">userMsg</span><span class="p">)</span>

    <span class="n">events</span><span class="p">.</span><span class="nf">blockingForEach</span><span class="p">(</span><span class="nc">Consumer</span> <span class="p">{</span> <span class="n">event</span><span class="p">:</span> <span class="nc">Event</span> <span class="p">-&gt;</span>
        <span class="n">event</span><span class="p">.</span><span class="nf">content</span><span class="p">().</span><span class="k">get</span><span class="p">().</span><span class="nf">parts</span><span class="p">().</span><span class="nf">getOrNull</span><span class="p">()</span><span class="o">?.</span><span class="nf">forEach</span> <span class="p">{</span> <span class="n">part</span> <span class="p">-&gt;</span>
            <span class="n">part</span><span class="p">.</span><span class="nf">text</span><span class="p">().</span><span class="nf">getOrNull</span><span class="p">()</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span> <span class="nf">println</span><span class="p">(</span><span class="n">it</span><span class="p">)</span> <span class="p">}</span>
            <span class="n">part</span><span class="p">.</span><span class="nf">functionCall</span><span class="p">().</span><span class="nf">getOrNull</span><span class="p">()</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span> <span class="nf">println</span><span class="p">(</span><span class="n">it</span><span class="p">)</span> <span class="p">}</span>
            <span class="n">part</span><span class="p">.</span><span class="nf">functionResponse</span><span class="p">().</span><span class="nf">getOrNull</span><span class="p">()</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span> <span class="nf">println</span><span class="p">(</span><span class="n">it</span><span class="p">)</span> <span class="p">}</span>
        <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="nf">errorCode</span><span class="p">().</span><span class="n">isPresent</span> <span class="p">||</span> <span class="n">event</span><span class="p">.</span><span class="nf">errorMessage</span><span class="p">().</span><span class="n">isPresent</span><span class="p">)</span> <span class="p">{</span>
            <span class="nf">println</span><span class="p">(</span><span class="s">"error: ${event.errorCode().get()}, ${event.errorMessage().get()}"</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">})</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>

<h3 id="tools">Tools</h3>

<p>A <strong>Tool</strong> in this context “represents a specific capability provided to an AI agent, enabling it to perform actions and interact with the world beyond its core text generation and reasoning abilities”.  Shown below are examples of creating both MCP and “Function Tools”.</p>

<h4 id="mcp-tools">MCP Tools</h4>

<p>The following shows an example of how we can create an MCP Server tool…the MCP Server in this case developed using the  <a href="https://github.com/modelcontextprotocol/kotlin-sdk">Kotlin MCP SDK</a>.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="code"><pre><span class="kd">val</span> <span class="py">tools</span> <span class="p">=</span> <span class="nc">McpToolset</span><span class="p">(</span>
    <span class="nc">ServerParameters</span>
        <span class="p">.</span><span class="nf">builder</span><span class="p">(</span><span class="s">"java"</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">args</span><span class="p">(</span><span class="s">"-jar"</span><span class="p">,</span> <span class="s">"&lt;path to climate trace mcp server jar file&gt;"</span><span class="p">,</span> <span class="s">"--stdio"</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
<span class="p">).</span><span class="nf">loadTools</span><span class="p">().</span><span class="nf">join</span><span class="p">()</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p />

<h4 id="function-tools">Function Tools</h4>

<p>We can also provide what are known as function tools (just local funtions that provide some functionality to our AI agent). Here we need to call Kotlin suspend functions from those tools and, as such, are specifically using ADK’s <a href="https://google.github.io/adk-docs/tools/function-tools/#2-long-running-function-tool">Long Running Function Tools</a>.  These need to return an RxJava <code class="language-plaintext highlighter-rouge">Single</code> so we need to use the following dependency to allow us to wrap invocation of our Kotlin suspend functions with <code class="language-plaintext highlighter-rouge">rxSingle</code>.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
</pre></td><td class="code"><pre><span class="nf">implementation</span> <span class="p">(</span><span class="s">"org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.10.2"</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>And these are the tools we’re using.  The methods need to be static to be consumed by the Java based ADK code so we include in a companion object and annotate with <code class="language-plaintext highlighter-rouge">@JvmStatic</code>.  Note were invoking shared KMP code here (<code class="language-plaintext highlighter-rouge">ClimateTraceRepository</code>) managed using the <code class="language-plaintext highlighter-rouge">Koin</code> DI framework.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="code"><pre><span class="kd">class</span> <span class="nc">ClimateTraceTool</span> <span class="p">{</span>
    <span class="k">companion</span> <span class="k">object</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">climateTraceRepository</span> <span class="p">=</span> <span class="n">koin</span><span class="p">.</span><span class="k">get</span><span class="p">&lt;</span><span class="nc">ClimateTraceRepository</span><span class="p">&gt;()</span>

        <span class="nd">@JvmStatic</span>
        <span class="k">fun</span> <span class="nf">getCountries</span><span class="p">():</span> <span class="nc">Single</span><span class="p">&lt;</span><span class="nc">Map</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">,</span> <span class="nc">String</span><span class="p">&gt;&gt;</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nf">rxSingle</span> <span class="p">{</span>
                <span class="nf">mapOf</span><span class="p">(</span><span class="s">"countries"</span> <span class="n">to</span> <span class="n">climateTraceRepository</span><span class="p">.</span><span class="nf">fetchCountries</span><span class="p">().</span><span class="nf">toString</span><span class="p">())</span>
            <span class="p">}</span>
        <span class="p">}</span>

        <span class="nd">@JvmStatic</span>
        <span class="k">fun</span> <span class="nf">getEmissions</span><span class="p">(</span><span class="n">countryCode</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">year</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">Single</span><span class="p">&lt;</span><span class="nc">Map</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">,</span> <span class="nc">String</span><span class="p">&gt;&gt;</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nf">rxSingle</span> <span class="p">{</span>
                <span class="nf">mapOf</span><span class="p">(</span><span class="s">"emissions"</span> <span class="n">to</span> <span class="n">climateTraceRepository</span><span class="p">.</span><span class="nf">fetchCountryEmissionsInfo</span><span class="p">(</span><span class="n">countryCode</span><span class="p">,</span> <span class="n">year</span><span class="p">).</span><span class="nf">toString</span><span class="p">())</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="code"><pre><span class="kd">val</span> <span class="py">getCountriesTool</span> <span class="p">=</span> <span class="nc">LongRunningFunctionTool</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="nc">ClimateTraceTool</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">,</span> <span class="s">"getCountries"</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">getEmissionsTool</span> <span class="p">=</span> <span class="nc">LongRunningFunctionTool</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="nc">ClimateTraceTool</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">,</span> <span class="s">"getEmissions"</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">tools</span> <span class="p">=</span> <span class="nf">listOf</span><span class="p">(</span><span class="n">getCountriesTool</span><span class="p">,</span> <span class="n">getEmissionsTool</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>

<h3 id="adk-dev-ui">ADK Dev UI</h3>

<p>ADK also includes a developer web UI to help with agent develoment and debugging.  It works by scanning a list of source folders provided to it for agents and this setup right now requires that we provide a Java file such as following.</p>

<h4 id="agentjava">Agent.java</h4>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="code"><pre><span class="k">public</span> <span class="kd">class</span> <span class="nc">Agent</span> <span class="p">{</span>
    <span class="k">public</span> <span class="n">static</span> <span class="nc">BaseAgent</span> <span class="nc">ROOT_AGENT</span> <span class="p">=</span> <span class="nf">initAgent</span><span class="p">();</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>We’ve also created a gradle task to allow launching that dev UI.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="code"><pre><span class="n">tasks</span><span class="p">.</span><span class="n">register</span><span class="p">&lt;</span><span class="nc">JavaExec</span><span class="p">&gt;(</span><span class="s">"devUi"</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">group</span> <span class="p">=</span> <span class="s">"application"</span>
    <span class="n">description</span> <span class="p">=</span> <span class="s">"Start the ADK Dev UI server"</span>
    <span class="n">mainClass</span><span class="p">.</span><span class="k">set</span><span class="p">(</span><span class="s">"com.google.adk.web.AdkWebServer"</span><span class="p">)</span>
    <span class="n">classpath</span> <span class="p">=</span> <span class="n">sourceSets</span><span class="p">[</span><span class="s">"main"</span><span class="p">].</span><span class="n">runtimeClasspath</span>
    <span class="n">args</span> <span class="p">=</span> <span class="nf">listOf</span><span class="p">(</span><span class="s">"--adk.agents.source-dir=src/main/java"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><strong>Update: 19th September 2005</strong></p>

<p>With ADK 0.3.0 the process of setting up ADK Dev UI has been simplified.</p>

<p><img src="/images/adk_dev_ui.jpeg" alt="ADK Dev UI" /></p>

<p><br />
Featured in <a href="https://androidweekly.net/issues/issue-683">Android Weekly #683</a></p>

<h3 id="related-tweet">Related tweet</h3>

<div class="jekyll-twitter-plugin"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Using Google&#39;s Agent Development Kit for Java from Kotlin code <a href="https://t.co/MABiUdhR6T">https://t.co/MABiUdhR6T</a><br /><br />Uses both MCP and local &quot;function tools&quot;....article includes some fun gotchas about calling suspend functions from those tools and working with the ADK Dev UI)!  <a href="https://twitter.com/hashtag/BuildWithAI?src=hash&amp;ref_src=twsrc%5Etfw">#BuildWithAI</a> cc <a href="https://twitter.com/meteatamel?ref_src=twsrc%5Etfw">@meteatamel</a></p>&mdash; John O&#39;Reilly (@joreilly) <a href="https://twitter.com/joreilly/status/1943738189404889552?ref_src=twsrc%5Etfw">July 11, 2025</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</div>]]></content><author><name></name></author><summary type="html"><![CDATA[Google’s Agent Development Kit for Java is described as “an open-source, code-first Java toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control”. In this article we’re going to see how it can be consumed in Kotlin code (specifically in the agents module in the ClimateTrace Kotlin Multiplatform sample).]]></summary></entry><entry><title type="html">Initial exploration of using Koog for developing Kotlin based AI agents</title><link href="https://johnoreilly.dev/posts/kotlin-koog/" rel="alternate" type="text/html" title="Initial exploration of using Koog for developing Kotlin based AI agents" /><published>2025-06-22T00:00:00+01:00</published><updated>2025-06-22T00:00:00+01:00</updated><id>https://johnoreilly.dev/posts/kotlin-koog</id><content type="html" xml:base="https://johnoreilly.dev/posts/kotlin-koog/"><![CDATA[<p><a href="https://github.com/JetBrains/koog">Koog</a>, announced recently at KotlinConf, is a new Kotlin-based framework designed to build and run AI agents.  This article will outline initial exploration of using Koog along with a number of configured MCP servers (specifically <a href="https://github.com/JetBrains/mcp-jetbrains">mcp-jetbrains</a> which we’ll use to control the IntelliJ/Android Studio IDE and also one based on the <a href="https://github.com/joreilly/ClimateTraceKMP">ClimateTraceKMP</a> Kotlin Multiplatform (KMP) sample built using the <a href="https://github.com/modelcontextprotocol/kotlin-sdk">Kotlin MCP SDK</a>).</p>

<div class="jekyll-twitter-plugin"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Koog: a Kotlin-based framework designed to build and run AI agents entirely in idiomatic Kotlin (just announced at <a href="https://twitter.com/hashtag/KotlinConf?src=hash&amp;ref_src=twsrc%5Etfw">#KotlinConf</a> keynote) <a href="https://t.co/TirxwuMRZ2">https://t.co/TirxwuMRZ2</a></p>&mdash; John O&#39;Reilly (@joreilly) <a href="https://twitter.com/joreilly/status/1925455659677483264?ref_src=twsrc%5Etfw">May 22, 2025</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</div>

<p>In this somewhat contrived example our agent will use a prompt that (1) requests climate emission data, (2) generates Compose UI code (and preview) that shows that data and, (3) adds file containing that code to an Android Studio project. It will also use the MCP servers we configured as needed. Koog can work with several different LLMs and we’ll use <a href="https://ai.google.dev/gemini-api/docs/models">Google Gemini</a> in this example.</p>

<h3 id="implementation">Implementation</h3>

<p>Koog is available through the following dependency</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
</pre></td><td class="code"><pre><span class="nf">implementation</span><span class="p">(</span><span class="s">"ai.koog:koog-agents:0.2.1"</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>The following then is all the code needed to create our initial basic AI agent (using Gemini LLM in this case).  This is based on using a “Single-run agent” (Koog also supports creation of more complex workflow agents).</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
</pre></td><td class="code"><pre><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">toolRegistry</span> <span class="p">=</span> <span class="nf">createAndStartMCPServers</span><span class="p">()</span>

    <span class="kd">val</span> <span class="py">agent</span> <span class="p">=</span> <span class="nc">AIAgent</span><span class="p">(</span>
        <span class="n">executor</span> <span class="p">=</span> <span class="nf">simpleGoogleAIExecutor</span><span class="p">(</span><span class="n">apiKeyGoogle</span><span class="p">),</span>
        <span class="n">llmModel</span> <span class="p">=</span> <span class="nc">GoogleModels</span><span class="p">.</span><span class="nc">Gemini1_5Pro</span><span class="p">,</span>
        <span class="n">toolRegistry</span> <span class="p">=</span> <span class="n">toolRegistry</span>
    <span class="p">)</span>

    <span class="n">agent</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span>
        <span class="s">"""
            Get emissions for France, Germany and Spain for 2023 and 2024.
            Create Compose UI file using Material3 showing the emissions data in a grid
            and add to the project in the composeApp module.
            Use units of millions for the emissions data.
            Also create a Compose preview.
        """</span><span class="p">.</span><span class="nf">trimIndent</span><span class="p">()</span>
    <span class="p">)</span>
<span class="p">}</span>

<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">createAndStartMCPServers</span><span class="p">():</span> <span class="nc">ToolRegistry</span> <span class="p">{</span>
    <span class="c1">// ClimateTrace MCP Server</span>
    <span class="kd">val</span> <span class="py">processClimateTrace</span> <span class="p">=</span> <span class="nc">ProcessBuilder</span>
        <span class="p">(</span><span class="s">"java"</span><span class="p">,</span> <span class="s">"-jar"</span><span class="p">,</span>
        <span class="s">"&lt;path to climate trace mcp server jar file&gt;"</span><span class="p">,</span>
        <span class="s">"--stdio"</span>
    <span class="p">).</span><span class="nf">start</span><span class="p">()</span>

    <span class="kd">val</span> <span class="py">transportClimateTrace</span> <span class="p">=</span> <span class="nc">McpToolRegistryProvider</span><span class="p">.</span><span class="nf">defaultStdioTransport</span><span class="p">(</span><span class="n">processClimateTrace</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">toolRegistryClimateTrace</span> <span class="p">=</span> <span class="nc">McpToolRegistryProvider</span><span class="p">.</span><span class="nf">fromTransport</span><span class="p">(</span><span class="n">transportClimateTrace</span><span class="p">)</span>

    <span class="c1">// JetBrains MCP server</span>
    <span class="kd">val</span> <span class="py">processIntelliJ</span> <span class="p">=</span> <span class="nc">ProcessBuilder</span><span class="p">(</span>
        <span class="s">"npx"</span><span class="p">,</span> <span class="s">"-y"</span><span class="p">,</span> <span class="s">"@jetbrains/mcp-proxy"</span>
    <span class="p">).</span><span class="nf">start</span><span class="p">()</span>

    <span class="kd">val</span> <span class="py">transportIntelliJ</span> <span class="p">=</span> <span class="nc">McpToolRegistryProvider</span><span class="p">.</span><span class="nf">defaultStdioTransport</span><span class="p">(</span><span class="n">processIntelliJ</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">toolRegistryIntelliJ</span> <span class="p">=</span> <span class="nc">McpToolRegistryProvider</span><span class="p">.</span><span class="nf">fromTransport</span><span class="p">(</span><span class="n">transportIntelliJ</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">toolRegistryClimateTrace</span> <span class="p">+</span> <span class="n">toolRegistryIntelliJ</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>This is the output when we run this code showing the various MCP server tool invocations made.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="code"><pre><span class="nc">Tool</span> <span class="n">called</span><span class="p">:</span> <span class="n">tool</span> <span class="k">get</span><span class="p">-</span><span class="n">emissions</span><span class="p">,</span> <span class="n">args</span> <span class="nc">Args</span><span class="p">(</span><span class="n">arguments</span><span class="p">={</span><span class="s">"countryCode"</span><span class="p">:</span><span class="s">"FRA"</span><span class="p">,</span><span class="s">"year"</span><span class="p">:</span><span class="s">"2023"</span><span class="p">})</span>
<span class="nc">Tool</span> <span class="n">called</span><span class="p">:</span> <span class="n">tool</span> <span class="k">get</span><span class="p">-</span><span class="n">emissions</span><span class="p">,</span> <span class="n">args</span> <span class="nc">Args</span><span class="p">(</span><span class="n">arguments</span><span class="p">={</span><span class="s">"countryCode"</span><span class="p">:</span><span class="s">"FRA"</span><span class="p">,</span><span class="s">"year"</span><span class="p">:</span><span class="s">"2024"</span><span class="p">})</span>
<span class="nc">Tool</span> <span class="n">called</span><span class="p">:</span> <span class="n">tool</span> <span class="k">get</span><span class="p">-</span><span class="n">emissions</span><span class="p">,</span> <span class="n">args</span> <span class="nc">Args</span><span class="p">(</span><span class="n">arguments</span><span class="p">={</span><span class="s">"countryCode"</span><span class="p">:</span><span class="s">"DEU"</span><span class="p">,</span><span class="s">"year"</span><span class="p">:</span><span class="s">"2023"</span><span class="p">})</span>
<span class="nc">Tool</span> <span class="n">called</span><span class="p">:</span> <span class="n">tool</span> <span class="k">get</span><span class="p">-</span><span class="n">emissions</span><span class="p">,</span> <span class="n">args</span> <span class="nc">Args</span><span class="p">(</span><span class="n">arguments</span><span class="p">={</span><span class="s">"year"</span><span class="p">:</span><span class="s">"2024"</span><span class="p">,</span><span class="s">"countryCode"</span><span class="p">:</span><span class="s">"DEU"</span><span class="p">})</span>
<span class="nc">Tool</span> <span class="n">called</span><span class="p">:</span> <span class="n">tool</span> <span class="k">get</span><span class="p">-</span><span class="n">emissions</span><span class="p">,</span> <span class="n">args</span> <span class="nc">Args</span><span class="p">(</span><span class="n">arguments</span><span class="p">={</span><span class="s">"countryCode"</span><span class="p">:</span><span class="s">"ESP"</span><span class="p">,</span><span class="s">"year"</span><span class="p">:</span><span class="s">"2023"</span><span class="p">})</span>
<span class="nc">Tool</span> <span class="n">called</span><span class="p">:</span> <span class="n">tool</span> <span class="k">get</span><span class="p">-</span><span class="n">emissions</span><span class="p">,</span> <span class="n">args</span> <span class="nc">Args</span><span class="p">(</span><span class="n">arguments</span><span class="p">={</span><span class="s">"year"</span><span class="p">:</span><span class="s">"2024"</span><span class="p">,</span><span class="s">"countryCode"</span><span class="p">:</span><span class="s">"ESP"</span><span class="p">})</span>
<span class="nc">Tool</span> <span class="n">called</span><span class="p">:</span> <span class="n">tool</span> <span class="n">create_new_file_with_text</span><span class="p">,</span> <span class="n">args</span> <span class="nc">Args</span><span class="p">(</span><span class="n">arguments</span><span class="p">={</span><span class="s">"text"</span><span class="p">:</span><span class="s">"```kotlin\npackage com.example.composeapp\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport com.example.composeapp.ui.theme.ComposeAppTheme\n\n\nclass MainActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            ComposeAppTheme {\n                // A surface container using the 'background' color from the theme\n                EmissionsGrid()\n\n            }\n        }\n    }\n}\n\n@Composable\nfun EmissionsGridItem(country: String, year: Int, emissions: Double) {\n    Card(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(4.dp),\n        colors = CardDefaults.cardColors(\n            containerColor = MaterialTheme.colorScheme.surfaceVariant,\n        ),\n\n        ) {\n        Column(\n            modifier = Modifier.padding(16.dp)\n\n        ) {\n\n            Text(text = country, style = MaterialTheme.typography.titleMedium)\n            Text(\n                text = \"Year: $year\",\n                style = MaterialTheme.typography.bodyMedium,\n                color = Color.Gray\n\n            )\n            Text(\n                text = \"Emissions (millions): ${emissions}\",\n                style = MaterialTheme.typography.bodyLarge\n            )\n        }\n    }\n}\n\n@Composable\nfun EmissionsGrid() {\n    val emissionsData = listOf(\n        EmissionsData(\"France\", 2023, 367.2409),\n        EmissionsData(\"France\", 2024, 366.7807),\n        EmissionsData(\"Germany\", 2023, 690.8744),\n        EmissionsData(\"Germany\", 2024, 671.735),\n        EmissionsData(\"Spain\", 2023, 273.4409),\n        EmissionsData(\"Spain\", 2024, 277.5746)\n\n    )\n    Column {\n        emissionsData.forEach { data -&gt;\n            EmissionsGridItem(data.country, data.year, data.emissions)\n        }\n    }\n}\n\n\ndata class EmissionsData(val country: String, val year: Int, val emissions: Double)\n\n\n@Preview(showBackground = true)\n@Composable\nfun DefaultPreview() {\n    ComposeAppTheme {\n        EmissionsGrid()\n    }\n}\n```"</span><span class="p">,</span><span class="s">"pathInProject"</span><span class="p">:</span><span class="s">"composeApp/src/main/java/com/example/composeapp/EmissionsView.kt"</span><span class="p">})</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>And this is example then of the Compose code (and associated preview) that was added to the Android Studio project (using the <a href="https://github.com/JetBrains/mcp-jetbrains">mcp-jetbrains</a> plugin).
<img src="/images/koog_example.png" alt="Koog Compose example" /></p>

<p>Koog can work with several different LLM providers (specifically Google, OpenAI, Anthropic, OpenRouter, and Ollama) and following shows how we can create an agent that uses for example OpenAI. You can also create prompt executors that works with multiple LLM providers.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="code"><pre><span class="kd">val</span> <span class="py">agent</span> <span class="p">=</span> <span class="nc">AIAgent</span><span class="p">(</span>
    <span class="n">executor</span> <span class="p">=</span> <span class="nf">simpleOpenAIExecutor</span><span class="p">(</span><span class="n">openAIApiKey</span><span class="p">),</span>
    <span class="n">llmModel</span> <span class="p">=</span> <span class="nc">OpenAIModels</span><span class="p">.</span><span class="nc">Chat</span><span class="p">.</span><span class="nc">GPT4o</span><span class="p">,</span>
    <span class="n">toolRegistry</span> <span class="p">=</span> <span class="n">toolRegistry</span>
<span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>

<h3 id="update-july-11th-2025">Update July 11th 2025</h3>

<p>Along with MCP based tools, Koog also supports having “local” annotation-based custom tools and the following is an example of such a tool (also merged to ClimateTraceKMP project).</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="code"><pre><span class="nd">@LLMDescription</span><span class="p">(</span><span class="s">"Tools for getting climate emission information"</span><span class="p">)</span>
<span class="kd">class</span> <span class="nc">ClimateTraceTool</span> <span class="p">:</span> <span class="nc">ToolSet</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">climateTraceRepository</span> <span class="p">=</span> <span class="n">koin</span><span class="p">.</span><span class="k">get</span><span class="p">&lt;</span><span class="nc">ClimateTraceRepository</span><span class="p">&gt;()</span>

    <span class="nd">@Tool</span>
    <span class="nd">@LLMDescription</span><span class="p">(</span><span class="s">"Get the list of countries"</span><span class="p">)</span>
    <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">getCountries</span><span class="p">():</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">&gt;</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">countries</span> <span class="p">=</span> <span class="n">climateTraceRepository</span><span class="p">.</span><span class="nf">fetchCountries</span><span class="p">()</span>
        <span class="k">return</span> <span class="n">countries</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="s">"${it.name}, ${it.alpha3}"</span> <span class="p">}</span>
    <span class="p">}</span>


    <span class="nd">@Tool</span><span class="p">(</span><span class="n">customName</span> <span class="p">=</span> <span class="s">"getEmissions"</span><span class="p">)</span>
    <span class="nd">@LLMDescription</span><span class="p">(</span><span class="s">"Get the emission data for a country for a particular year."</span><span class="p">)</span>
    <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">getEmissions</span><span class="p">(</span>
        <span class="nd">@LLMDescription</span><span class="p">(</span><span class="s">"country code"</span><span class="p">)</span> <span class="n">countryCode</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span>
        <span class="nd">@LLMDescription</span><span class="p">(</span><span class="s">"year"</span><span class="p">)</span> <span class="n">year</span><span class="p">:</span> <span class="nc">String</span>
    <span class="p">):</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">&gt;</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">climateTraceRepository</span><span class="p">.</span><span class="nf">fetchCountryEmissionsInfo</span><span class="p">(</span><span class="n">countryCode</span><span class="p">,</span> <span class="n">year</span><span class="p">).</span><span class="nf">map</span> <span class="p">{</span>
            <span class="n">it</span><span class="p">.</span><span class="n">emissions</span><span class="p">.</span><span class="n">co2</span><span class="p">.</span><span class="nf">toString</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>With that implemented we can now switch between adding this or the MCP based tool to the tool registry.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="code"><pre><span class="k">suspend</span> <span class="k">fun</span> <span class="nf">createToolSetRegistry</span><span class="p">():</span> <span class="nc">ToolRegistry</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">processClimateTrace</span> <span class="p">=</span> <span class="nc">ProcessBuilder</span><span class="p">(</span><span class="s">"java"</span><span class="p">,</span> <span class="s">"-jar"</span><span class="p">,</span>
        <span class="s">"&lt;path to climate trace mcp server jar file&gt;"</span><span class="p">,</span> <span class="s">"--stdio"</span>
    <span class="p">).</span><span class="nf">start</span><span class="p">()</span>
    <span class="kd">val</span> <span class="py">transportClimateTrace</span> <span class="p">=</span> <span class="nc">McpToolRegistryProvider</span><span class="p">.</span><span class="nf">defaultStdioTransport</span><span class="p">(</span><span class="n">processClimateTrace</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">toolRegistryClimateTrace</span> <span class="p">=</span> <span class="nc">McpToolRegistryProvider</span><span class="p">.</span><span class="nf">fromTransport</span><span class="p">(</span><span class="n">transportClimateTrace</span><span class="p">)</span>

    <span class="kd">val</span> <span class="py">localToolSetRegistry</span> <span class="p">=</span> <span class="nc">ToolRegistry</span> <span class="p">{</span> <span class="nf">tools</span><span class="p">(</span><span class="nc">ClimateTraceTool</span><span class="p">().</span><span class="nf">asTools</span><span class="p">())</span> <span class="p">}</span>

    <span class="c1">// Can use either local toolset or one based on MCP server</span>
    <span class="c1">//return toolRegistryClimateTrace</span>
    <span class="k">return</span> <span class="n">localToolSetRegistry</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br />
Featured in <a href="https://mailchi.mp/kotlinweekly/kotlin-weekly-465">Kotlin Weekly Issue #465</a> and <a href="https://androidweekly.net/issues/issue-681">Android Weekly #681</a></p>

<h3 id="related-tweet">Related tweet</h3>

<div class="jekyll-twitter-plugin"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Wrote short article about use of Koog for developing Kotlin based AI agents (combined with a number of MCP servers).<br /><br />The example included is based on use of <a href="https://twitter.com/hashtag/BuildWithGemini?src=hash&amp;ref_src=twsrc%5Etfw">#BuildWithGemini</a> LLM (really had to use that with <a href="https://twitter.com/hashtag/GoogleIOConnect?src=hash&amp;ref_src=twsrc%5Etfw">#GoogleIOConnect</a> fast approaching 😀) <a href="https://t.co/rd7X5vFWvp">https://t.co/rd7X5vFWvp</a></p>&mdash; John O&#39;Reilly (@joreilly) <a href="https://twitter.com/joreilly/status/1936857719329222841?ref_src=twsrc%5Etfw">June 22, 2025</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</div>]]></content><author><name></name></author><summary type="html"><![CDATA[Koog, announced recently at KotlinConf, is a new Kotlin-based framework designed to build and run AI agents. This article will outline initial exploration of using Koog along with a number of configured MCP servers (specifically mcp-jetbrains which we’ll use to control the IntelliJ/Android Studio IDE and also one based on the ClimateTraceKMP Kotlin Multiplatform (KMP) sample built using the Kotlin MCP SDK).]]></summary></entry><entry><title type="html">Kotlin MCP 💜 Kotlin Multiplatform</title><link href="https://johnoreilly.dev/posts/kotlin-mcp-kmp/" rel="alternate" type="text/html" title="Kotlin MCP 💜 Kotlin Multiplatform" /><published>2025-05-31T00:00:00+01:00</published><updated>2025-05-31T00:00:00+01:00</updated><id>https://johnoreilly.dev/posts/kotlin-mcp-kmp</id><content type="html" xml:base="https://johnoreilly.dev/posts/kotlin-mcp-kmp/"><![CDATA[<p>MCP (Model Context Protocol) is a relatively new open standard that allows AI assistants to connect with external data sources and tools.  In this article we’re going to show how we can use the <a href="https://github.com/modelcontextprotocol/kotlin-sdk">Kotlin MCP SDK</a> to create an MCP Server which in turn uses Kotlin Multiplatform (KMP) shared code to obtain the data that the server is providing. Specifically we’re going to add an MCP module to the <a href="https://github.com/joreilly/ClimateTraceKMP">ClimateTrace</a> KMP sample.</p>

<h3 id="kotlin-mcp-module">Kotlin MCP module</h3>

<p>We’re making use of the following dependency (for the <a href="https://github.com/modelcontextprotocol/kotlin-sdk">Kotlin MCP SDK</a>)</p>

<h4 id="libsversionstoml">libs.versions.toml</h4>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
</pre></td><td class="code"><pre><span class="n">mcp</span> <span class="p">=</span> <span class="s">"0.5.0"</span>
<span class="n">mcp-kotlin</span> <span class="p">=</span> <span class="p">{</span> <span class="n">group</span> <span class="p">=</span> <span class="s">"io.modelcontextprotocol"</span><span class="p">,</span> <span class="n">name</span> <span class="p">=</span> <span class="s">"kotlin-sdk"</span><span class="p">,</span> <span class="n">version</span><span class="p">.</span><span class="n">ref</span> <span class="p">=</span> <span class="s">"mcp"</span> <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Having added that we can now create our MCP Server instance.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="code"><pre><span class="kd">val</span> <span class="py">server</span> <span class="p">=</span> <span class="nc">Server</span><span class="p">(</span>
    <span class="nc">Implementation</span><span class="p">(</span>
        <span class="n">name</span> <span class="p">=</span> <span class="s">"ClimateTrace MCP Server"</span><span class="p">,</span>
        <span class="n">version</span> <span class="p">=</span> <span class="s">"1.0.0"</span>
    <span class="p">),</span>
    <span class="nc">ServerOptions</span><span class="p">(</span>
        <span class="n">capabilities</span> <span class="p">=</span> <span class="nc">ServerCapabilities</span><span class="p">(</span>
            <span class="n">tools</span> <span class="p">=</span> <span class="nc">ServerCapabilities</span><span class="p">.</span><span class="nc">Tools</span><span class="p">(</span><span class="n">listChanged</span> <span class="p">=</span> <span class="k">true</span><span class="p">)</span>
        <span class="p">)</span>
    <span class="p">)</span>
<span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>That can be used in turn to add the following <strong>tools</strong> that we’ll be exposing from the server (MCP also supports exposing <strong>resources</strong> and <strong>prompts</strong>).</p>

<p><code class="language-plaintext highlighter-rouge">get-countries</code></p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="code"><pre><span class="n">server</span><span class="p">.</span><span class="nf">addTool</span><span class="p">(</span>
    <span class="n">name</span> <span class="p">=</span> <span class="s">"get-countries"</span><span class="p">,</span>
    <span class="n">description</span> <span class="p">=</span> <span class="s">"List of countries"</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">countries</span> <span class="p">=</span> <span class="n">climateTraceRepository</span><span class="p">.</span><span class="nf">fetchCountries</span><span class="p">()</span>
    <span class="nc">CallToolResult</span><span class="p">(</span>
        <span class="n">content</span> <span class="p">=</span> <span class="n">countries</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="nc">TextContent</span><span class="p">(</span><span class="s">"${it.name}, ${it.alpha3}"</span><span class="p">)</span> <span class="p">}</span>
    <span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><code class="language-plaintext highlighter-rouge">get-emissions</code></p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
</pre></td><td class="code"><pre><span class="n">server</span><span class="p">.</span><span class="nf">addTool</span><span class="p">(</span>
    <span class="n">name</span> <span class="p">=</span> <span class="s">"get-emissions"</span><span class="p">,</span>
    <span class="n">description</span> <span class="p">=</span> <span class="s">"List emission info for a particular country"</span><span class="p">,</span>
    <span class="n">inputSchema</span> <span class="p">=</span> <span class="nc">Tool</span><span class="p">.</span><span class="nc">Input</span><span class="p">(</span>
        <span class="n">properties</span> <span class="p">=</span> <span class="nc">JsonObject</span><span class="p">(</span>
            <span class="nf">mapOf</span><span class="p">(</span>
                <span class="s">"countryCode"</span> <span class="n">to</span> <span class="nc">JsonPrimitive</span><span class="p">(</span><span class="s">"string"</span><span class="p">),</span>
                <span class="s">"year"</span> <span class="n">to</span> <span class="nc">JsonPrimitive</span><span class="p">(</span><span class="s">"date"</span><span class="p">),</span>
            <span class="p">)</span>
        <span class="p">),</span>
        <span class="n">required</span> <span class="p">=</span> <span class="nf">listOf</span><span class="p">(</span><span class="s">"countryCode"</span><span class="p">,</span> <span class="s">"year"</span><span class="p">)</span>
    <span class="p">)</span>

<span class="p">)</span> <span class="p">{</span> <span class="n">request</span> <span class="p">-&gt;</span>
    <span class="kd">val</span> <span class="py">countryCode</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">arguments</span><span class="p">[</span><span class="s">"countryCode"</span><span class="p">]</span>
    <span class="kd">val</span> <span class="py">year</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">arguments</span><span class="p">[</span><span class="s">"year"</span><span class="p">]</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">countryCode</span> <span class="p">==</span> <span class="k">null</span> <span class="p">||</span> <span class="n">year</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span><span class="nd">@addTool</span> <span class="nc">CallToolResult</span><span class="p">(</span>
            <span class="n">content</span> <span class="p">=</span> <span class="nf">listOf</span><span class="p">(</span><span class="nc">TextContent</span><span class="p">(</span><span class="s">"The 'countryCode' and `year` parameters are required."</span><span class="p">))</span>
        <span class="p">)</span>
    <span class="p">}</span>

    <span class="kd">val</span> <span class="py">countryEmissionInfo</span> <span class="p">=</span> <span class="n">climateTraceRepository</span><span class="p">.</span><span class="nf">fetchCountryEmissionsInfo</span><span class="p">(</span>
        <span class="n">countryCode</span> <span class="p">=</span> <span class="n">countryCode</span><span class="p">.</span><span class="n">jsonPrimitive</span><span class="p">.</span><span class="n">content</span><span class="p">,</span>
        <span class="n">year</span> <span class="p">=</span> <span class="n">year</span><span class="p">.</span><span class="n">jsonPrimitive</span><span class="p">.</span><span class="n">content</span>
    <span class="p">)</span>

    <span class="nc">CallToolResult</span><span class="p">(</span>
        <span class="n">content</span> <span class="p">=</span> <span class="n">countryEmissionInfo</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="nc">TextContent</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">emissions</span><span class="p">.</span><span class="n">co2</span><span class="p">.</span><span class="nf">toString</span><span class="p">())</span> <span class="p">}</span>
    <span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Note that we’re making use of <code class="language-plaintext highlighter-rouge">ClimateTraceRepository</code> from our shared KMP code to fetch the data needed.</p>

<p>MCP servers are typically accessed over <code class="language-plaintext highlighter-rouge">stdio</code> or <code class="language-plaintext highlighter-rouge">sse</code>.  In the <code class="language-plaintext highlighter-rouge">stdio</code> case (as used by Claude Desktop for example) we run the following (wrapping around <code class="language-plaintext highlighter-rouge">server</code> instance we created above).</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="code"><pre><span class="kd">val</span> <span class="py">transport</span> <span class="p">=</span> <span class="nc">StdioServerTransport</span><span class="p">(</span>
    <span class="nc">System</span><span class="p">.</span><span class="n">`in`</span><span class="p">.</span><span class="nf">asInput</span><span class="p">(),</span>
    <span class="nc">System</span><span class="p">.</span><span class="k">out</span><span class="p">.</span><span class="nf">asSink</span><span class="p">().</span><span class="nf">buffered</span><span class="p">()</span>
<span class="p">)</span>

<span class="n">server</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="n">transport</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>In the case of <code class="language-plaintext highlighter-rouge">sse</code> we use <code class="language-plaintext highlighter-rouge">Ktor</code> server as follows.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="code"><pre><span class="nf">embeddedServer</span><span class="p">(</span><span class="nc">CIO</span><span class="p">,</span> <span class="n">host</span> <span class="p">=</span> <span class="s">"0.0.0.0"</span><span class="p">,</span> <span class="n">port</span> <span class="p">=</span> <span class="n">port</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">mcp</span> <span class="p">{</span>
        <span class="n">server</span>
    <span class="p">}</span>
<span class="p">}.</span><span class="nf">start</span><span class="p">(</span><span class="n">wait</span> <span class="p">=</span> <span class="k">true</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>

<h3 id="claude-desktop-integration">Claude Desktop Integration</h3>

<p>Now we can use <a href="https://claude.ai/download">Claude Desktop</a> to test out the integration.  We  add our MCP server by selecting <strong>Edit Config</strong> under developer settings. This will allow editing of <code class="language-plaintext highlighter-rouge">claude_desktop_config.json</code> to which we’ll add the following.  This will trigger Claude to start our server when it starts up (using jar file created using the gradle <code class="language-plaintext highlighter-rouge">:mcp-server:shadowJar</code> task`).</p>

<figure class="highlight"><pre><code class="language-json" data-lang="json"><span class="p">{</span><span class="w">
  </span><span class="nl">"mcpServers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"kotlin-climatetrace"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"java"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="s2">"-jar"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"/Users/joreilly/dev/github/ClimateTrace/mcp-server/build/libs/serverAll.jar"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"--stdio"</span><span class="w">
      </span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p><br />
With this in place we can now make climate emission related queries in Claude that will make use, as needed, of the tools we’re exposing from our MCP Server.  For example here we’re asking to “Graph France’s emissions from 2020 to 2024”. Note that Claude knew to initially call <code class="language-plaintext highlighter-rouge">get-countries</code> to get the country code it needed  for call to <code class="language-plaintext highlighter-rouge">get-emissions</code>.</p>

<p><img src="/images/claude_desktop_mcp.png" alt="Claude Desktop" /></p>

<p>Claude will also obtain other data it needs as shown for the query below (e.g. it separately fetched population data for those years in response to the query and combined with data served from the MCP server).</p>

<p><img src="/images/claude_desktop_mcp2.png" alt="Claude Desktop" /></p>

<p>Note that this exploration was heavily influenced by <a href="https://github.com/Kotlin/Kotlin-AI-Examples/tree/master/projects/mcp/mcp-demo">this very nice tutorial</a> from the official <strong>Kotlin-AI-Examples</strong> github repo.</p>

<p><br />
Featured in <a href="https://mailchi.mp/kotlinweekly/kotlin-weekly-461">Kotlin Weekly Issue #461</a> and <a href="https://androidweekly.net/issues/issue-678">Android Weekly #678</a></p>

<h3 id="related-tweet">Related tweet</h3>

<div class="jekyll-twitter-plugin"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Kotlin MCP 💜 Kotlin Multiplatform<br /><br />Wrote a short article on using the Kotlin MCP SDK in a KMP project  <a href="https://t.co/J2RU9fmlyA">https://t.co/J2RU9fmlyA</a></p>&mdash; John O&#39;Reilly (@joreilly) <a href="https://twitter.com/joreilly/status/1928931321985859716?ref_src=twsrc%5Etfw">May 31, 2025</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</div>]]></content><author><name></name></author><summary type="html"><![CDATA[MCP (Model Context Protocol) is a relatively new open standard that allows AI assistants to connect with external data sources and tools. In this article we’re going to show how we can use the Kotlin MCP SDK to create an MCP Server which in turn uses Kotlin Multiplatform (KMP) shared code to obtain the data that the server is providing. Specifically we’re going to add an MCP module to the ClimateTrace KMP sample.]]></summary></entry><entry><title type="html">Using Vertex AI in a Compose/Kotlin Multiplatform project</title><link href="https://johnoreilly.dev/posts/vertex-ai-kmp/" rel="alternate" type="text/html" title="Using Vertex AI in a Compose/Kotlin Multiplatform project" /><published>2024-10-27T00:00:00+01:00</published><updated>2024-10-27T00:00:00+01:00</updated><id>https://johnoreilly.dev/posts/vertex-ai-kmp</id><content type="html" xml:base="https://johnoreilly.dev/posts/vertex-ai-kmp/"><![CDATA[<p><strong>Update 2nd April 2025</strong>: Article now includes code for image generation and also cleaner approach for injecting iOS version of code using Koin.</p>

<p><br /></p>

<p>The general availability of <a href="https://firebase.google.com/docs/vertex-ai">Vertex AI in Firebase</a> was announced recently and in this article we’ll show how to use the associated Android and iOS SDKs in a Compose/Kotlin Multiplatform
project.  The code shown is included in the <a href="https://github.com/joreilly/VertexAI-KMP-Sample">VertexAI-KMP-Sample</a> repository.</p>

<div class="jekyll-twitter-plugin"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Vertex AI in Firebase is now generally available.<br /><br />You can confidently use our Kotlin, Swift, Web and Flutter SDKs to release your AI features into production, knowing they&#39;re backed by Google Cloud and Vertex AI quality standards.<br /><br />Discover more  ↓ <a href="https://t.co/SoOfAT9SOz">https://t.co/SoOfAT9SOz</a></p>&mdash; Firebase (@Firebase) <a href="https://twitter.com/Firebase/status/1848439110081335516?ref_src=twsrc%5Etfw">October 21, 2024</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</div>

<p />
<blockquote>

</blockquote>

<h2 id="setup">Setup</h2>

<p>We initially performed the following steps</p>

<ul>
  <li>Created a new Compose/Kotlin multiplatform project using the <a href="https://kmp.jetbrains.com/">Kotlin Multiplatform Wizard</a> (with “Share UI” option enabled).</li>
  <li>Created a new Firebase project and <a href="https://console.firebase.google.com/project/_/genai">enabled use of Vertex AI</a> for that project.</li>
  <li>Added Android and iOS apps in the Firebase console.  The associated <code class="language-plaintext highlighter-rouge">google-services.json</code> and <code class="language-plaintext highlighter-rouge">GoogleService-Info.plist</code> files were downloaded then and added to the Android and iOS projects.</li>
</ul>

<p style="text-align:center;"><img src="/images/vertexai_firebase.png" alt="Vertex AI firebase screenshot" width="400" /></p>

<h3 id="shared-kmp-code-setup">Shared KMP code setup</h3>

<p>We’re making use of the following libraries in shared code and made related changes shown below to the build config for the KMP module.</p>
<ul>
  <li><strong>Firebase</strong> (for the Vertex AI APIs)</li>
  <li><strong>Kotlinx Serialization</strong> (for parsing Vertex json response)</li>
  <li><strong>Markdown</strong> (for displaying Vertex markdown response)</li>
  <li><strong>Coil</strong> (CMP library for rendering images)</li>
  <li><strong>Koin</strong> (dependency injection along with support for KMP ViewModel)</li>
</ul>

<p />

<h4 id="libsversiontoml">libs.version.toml</h4>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
</pre></td><td class="code"><pre><span class="na">[versions]</span>
<span class="o">..</span><span class="p">.</span>
<span class="n">firebaseBom</span> <span class="p">=</span> <span class="s">"33.12.0"</span>
<span class="n">koin</span> <span class="p">=</span> <span class="s">"4.0.4"</span>
<span class="n">kotlinx-serialization</span> <span class="p">=</span> <span class="s">"1.8.0"</span>
<span class="n">markdownRenderer</span> <span class="p">=</span> <span class="s">"0.27.0"</span>
<span class="n">coil</span> <span class="p">=</span> <span class="s">"3.1.0"</span>
<span class="na">
[libraries]</span>
<span class="o">..</span><span class="p">.</span>
<span class="n">firebase-bom</span> <span class="p">=</span> <span class="p">{</span> <span class="n">module</span> <span class="p">=</span> <span class="s">"com.google.firebase:firebase-bom"</span><span class="p">,</span> <span class="n">version</span><span class="p">.</span><span class="n">ref</span> <span class="p">=</span> <span class="s">"firebaseBom"</span> <span class="p">}</span>
<span class="n">firebase-vertexai</span> <span class="p">=</span> <span class="p">{</span> <span class="n">module</span> <span class="p">=</span> <span class="s">"com.google.firebase:firebase-vertexai"</span> <span class="p">}</span>
<span class="n">coil-compose</span> <span class="p">=</span> <span class="p">{</span> <span class="n">module</span> <span class="p">=</span> <span class="s">"io.coil-kt.coil3:coil-compose"</span><span class="p">,</span> <span class="n">version</span><span class="p">.</span><span class="n">ref</span> <span class="p">=</span> <span class="s">"coil"</span> <span class="p">}</span>
<span class="n">koin-core</span> <span class="p">=</span> <span class="p">{</span> <span class="n">module</span> <span class="p">=</span> <span class="s">"io.insert-koin:koin-core"</span><span class="p">,</span> <span class="n">version</span><span class="p">.</span><span class="n">ref</span> <span class="p">=</span> <span class="s">"koin"</span> <span class="p">}</span>
<span class="n">koin-compose-viewmodel</span> <span class="p">=</span> <span class="p">{</span> <span class="n">module</span> <span class="p">=</span> <span class="s">"io.insert-koin:koin-compose-viewmodel"</span><span class="p">,</span> <span class="n">version</span><span class="p">.</span><span class="n">ref</span> <span class="p">=</span> <span class="s">"koin"</span> <span class="p">}</span>
<span class="n">kotlinx-serialization</span> <span class="p">=</span> <span class="p">{</span> <span class="n">group</span> <span class="p">=</span> <span class="s">"org.jetbrains.kotlinx"</span><span class="p">,</span> <span class="n">name</span> <span class="p">=</span> <span class="s">"kotlinx-serialization-core"</span><span class="p">,</span> <span class="n">version</span><span class="p">.</span><span class="n">ref</span> <span class="p">=</span> <span class="s">"kotlinx-serialization"</span> <span class="p">}</span>
<span class="n">kotlinx-serialization-json</span> <span class="p">=</span> <span class="p">{</span> <span class="n">group</span> <span class="p">=</span> <span class="s">"org.jetbrains.kotlinx"</span><span class="p">,</span> <span class="n">name</span> <span class="p">=</span> <span class="s">"kotlinx-serialization-json"</span><span class="p">,</span> <span class="n">version</span><span class="p">.</span><span class="n">ref</span> <span class="p">=</span> <span class="s">"kotlinx-serialization"</span> <span class="p">}</span>
<span class="n">markdown-renderer</span> <span class="p">=</span> <span class="p">{</span> <span class="n">module</span> <span class="p">=</span> <span class="s">"com.mikepenz:multiplatform-markdown-renderer-m3"</span><span class="p">,</span> <span class="n">version</span><span class="p">.</span><span class="n">ref</span> <span class="p">=</span> <span class="s">"markdownRenderer"</span> <span class="p">}</span>
<span class="na">

[plugins]</span>
<span class="o">..</span><span class="p">.</span>
<span class="n">googleServices</span> <span class="p">=</span> <span class="p">{</span> <span class="n">id</span> <span class="p">=</span> <span class="s">"com.google.gms.google-services"</span><span class="p">,</span> <span class="n">version</span><span class="p">.</span><span class="n">ref</span> <span class="p">=</span> <span class="s">"googleServices"</span> <span class="p">}</span>
<span class="n">kotlinxSerialization</span> <span class="p">=</span> <span class="p">{</span> <span class="n">id</span> <span class="p">=</span> <span class="s">"org.jetbrains.kotlin.plugin.serialization"</span><span class="p">,</span> <span class="n">version</span><span class="p">.</span><span class="n">ref</span> <span class="p">=</span> <span class="s">"kotlin"</span> <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p />

<h4 id="buildgradlekts">build.gradle.kts</h4>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="code"><pre><span class="nf">plugins</span> <span class="p">{</span>
    <span class="o">..</span><span class="p">.</span>
    <span class="nf">alias</span><span class="p">(</span><span class="n">libs</span><span class="p">.</span><span class="n">plugins</span><span class="p">.</span><span class="n">kotlinxSerialization</span><span class="p">)</span>
    <span class="nf">alias</span><span class="p">(</span><span class="n">libs</span><span class="p">.</span><span class="n">plugins</span><span class="p">.</span><span class="n">googleServices</span><span class="p">)</span>
<span class="p">}</span>

<span class="n">androidMain</span><span class="p">.</span><span class="nf">dependencies</span> <span class="p">{</span>
    <span class="o">..</span><span class="p">.</span>
    <span class="nf">implementation</span><span class="p">(</span><span class="n">project</span><span class="p">.</span><span class="n">dependencies</span><span class="p">.</span><span class="nf">platform</span><span class="p">(</span><span class="n">libs</span><span class="p">.</span><span class="n">firebase</span><span class="p">.</span><span class="n">bom</span><span class="p">))</span>
    <span class="nf">implementation</span><span class="p">(</span><span class="n">libs</span><span class="p">.</span><span class="n">firebase</span><span class="p">.</span><span class="n">vertexai</span><span class="p">)</span>
<span class="p">}</span>


<span class="n">commonMain</span><span class="p">.</span><span class="nf">dependencies</span> <span class="p">{</span>
    <span class="o">..</span><span class="p">.</span>
    <span class="nf">implementation</span><span class="p">(</span><span class="n">libs</span><span class="p">.</span><span class="n">kotlinx</span><span class="p">.</span><span class="n">serialization</span><span class="p">)</span>
    <span class="nf">implementation</span><span class="p">(</span><span class="n">libs</span><span class="p">.</span><span class="n">kotlinx</span><span class="p">.</span><span class="n">serialization</span><span class="p">.</span><span class="n">json</span><span class="p">)</span>
    <span class="nf">implementation</span><span class="p">(</span><span class="n">libs</span><span class="p">.</span><span class="n">markdown</span><span class="p">.</span><span class="n">renderer</span><span class="p">)</span>
    <span class="nf">implementation</span><span class="p">(</span><span class="n">libs</span><span class="p">.</span><span class="n">koin</span><span class="p">.</span><span class="n">core</span><span class="p">)</span>
    <span class="nf">implementation</span><span class="p">(</span><span class="n">libs</span><span class="p">.</span><span class="n">koin</span><span class="p">.</span><span class="n">compose</span><span class="p">.</span><span class="n">viewmodel</span><span class="p">)</span>
    <span class="nf">implementation</span><span class="p">(</span><span class="n">libs</span><span class="p">.</span><span class="n">coil</span><span class="p">.</span><span class="n">compose</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>

<h2 id="using-vertex-ai">Using Vertex AI</h2>

<p>We’re making use of 3 Vertex AI features in this sample (illustrated in the screenshots below)</p>
<ul>
  <li>text generation with markdown response (rendered using <code class="language-plaintext highlighter-rouge">Markdown</code> CMP library)</li>
  <li>structured json generation (with custom rendering of the result)</li>
  <li>image generation (using Imagen 3)</li>
</ul>

<p><img src="/images/vertex_sample_screenshots.png" alt="Vertex AI sample screenshots" /></p>

<p />

<p>To support Android and iOS specific implementations of this functionality we created the following interface in shared (<code class="language-plaintext highlighter-rouge">commonMain</code> code)</p>

<h4 id="generativemodelkt">GenerativeModel.kt</h4>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="code"><pre><span class="kd">interface</span> <span class="nc">GenerativeModel</span> <span class="p">{</span>
    <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">generateTextContent</span><span class="p">(</span><span class="n">prompt</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">String</span><span class="p">?</span>
    <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">generateJsonContent</span><span class="p">(</span><span class="n">prompt</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">String</span><span class="p">?</span>
    <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">generateImage</span><span class="p">(</span><span class="n">prompt</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">ByteArray</span><span class="p">?</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>This is implemented on Android as follows (in <code class="language-plaintext highlighter-rouge">androidMain</code> source set in shared KMP module) where we make use of the Vertex AI Android SDK. We’re also defining the schema here that Vertex will use when generating the json response.</p>

<p>For this sample we’re using a specific schema that supports a range of prompts that return a list of people (for example as shown in screenshots above).  The response is parsed using the Kotlinx Serializaton library and rendered as a simple list in our shared Compose Multiplatform UI code.</p>

<h4 id="generativemodelandroidkt">GenerativeModel.android.kt</h4>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
</pre></td><td class="code"><pre><span class="kd">class</span> <span class="nc">GenerativeModelAndroid</span><span class="p">:</span> <span class="nc">GenerativeModel</span> <span class="p">{</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">jsonSchema</span> <span class="p">=</span> <span class="nc">Schema</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span>
        <span class="nc">Schema</span><span class="p">.</span><span class="nf">obj</span><span class="p">(</span>
            <span class="nf">mapOf</span><span class="p">(</span>
                <span class="s">"name"</span> <span class="n">to</span> <span class="nc">Schema</span><span class="p">.</span><span class="nf">string</span><span class="p">(),</span>
                <span class="s">"country"</span> <span class="n">to</span> <span class="nc">Schema</span><span class="p">.</span><span class="nf">string</span><span class="p">()</span>
            <span class="p">)</span>
        <span class="p">)</span>
    <span class="p">)</span>

    <span class="k">override</span> <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">generateTextContent</span><span class="p">(</span><span class="n">prompt</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">String</span><span class="p">?</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">generativeModel</span> <span class="p">=</span> <span class="nc">Firebase</span><span class="p">.</span><span class="n">vertexAI</span><span class="p">.</span><span class="nf">generativeModel</span><span class="p">(</span>
            <span class="n">modelName</span> <span class="p">=</span> <span class="s">"gemini-1.5-flash"</span>
        <span class="p">)</span>

        <span class="k">return</span> <span class="n">generativeModel</span><span class="p">.</span><span class="nf">generateContent</span><span class="p">(</span><span class="n">prompt</span><span class="p">).</span><span class="n">text</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">generateJsonContent</span><span class="p">(</span><span class="n">prompt</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">String</span><span class="p">?</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">generativeModel</span> <span class="p">=</span> <span class="nc">Firebase</span><span class="p">.</span><span class="n">vertexAI</span><span class="p">.</span><span class="nf">generativeModel</span><span class="p">(</span>
            <span class="n">modelName</span> <span class="p">=</span> <span class="s">"gemini-1.5-flash"</span><span class="p">,</span>
            <span class="n">generationConfig</span> <span class="p">=</span> <span class="nf">generationConfig</span> <span class="p">{</span>
                <span class="n">responseMimeType</span> <span class="p">=</span> <span class="s">"application/json"</span>
                <span class="n">responseSchema</span> <span class="p">=</span> <span class="n">jsonSchema</span>
            <span class="p">}</span>
        <span class="p">)</span>

        <span class="k">return</span> <span class="n">generativeModel</span><span class="p">.</span><span class="nf">generateContent</span><span class="p">(</span><span class="n">prompt</span><span class="p">).</span><span class="n">text</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">generateImage</span><span class="p">(</span><span class="n">prompt</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">ByteArray</span><span class="p">?</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">imageModel</span> <span class="p">=</span> <span class="nc">Firebase</span><span class="p">.</span><span class="n">vertexAI</span><span class="p">.</span><span class="nf">imagenModel</span><span class="p">(</span>
            <span class="n">modelName</span> <span class="p">=</span> <span class="s">"imagen-3.0-generate-002"</span>
        <span class="p">)</span>
        <span class="kd">val</span> <span class="py">imageResponse</span> <span class="p">=</span> <span class="n">imageModel</span><span class="p">.</span><span class="nf">generateImages</span><span class="p">(</span><span class="n">prompt</span><span class="p">)</span>
        <span class="k">return</span> <span class="k">if</span> <span class="p">(</span><span class="n">imageResponse</span><span class="p">.</span><span class="n">images</span><span class="p">.</span><span class="nf">isNotEmpty</span><span class="p">())</span> <span class="p">{</span>
            <span class="n">imageResponse</span><span class="p">.</span><span class="n">images</span><span class="p">.</span><span class="nf">first</span><span class="p">().</span><span class="n">data</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">null</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>On iOS this is implemented in the following Swift code (using the Vertex AI iOS SDK in this case).  We’re also defining the above mentioned schema here.  A future enhancement would be to define this schema in some generic format in shared code and then translate that in the Android and iOS implementations.</p>

<h4 id="generativemodelios">GenerativeModelIOS</h4>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
</pre></td><td class="code"><pre><span class="kd">class</span> <span class="kt">GenerativeModelIOS</span><span class="p">:</span> <span class="kt">ComposeApp</span><span class="o">.</span><span class="kt">GenerativeModel</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="k">let</span> <span class="nv">shared</span> <span class="o">=</span> <span class="kt">GenerativeModelIOS</span><span class="p">()</span>

    <span class="k">let</span> <span class="nv">vertex</span> <span class="o">=</span> <span class="kt">VertexAI</span><span class="o">.</span><span class="nf">vertexAI</span><span class="p">()</span>

    <span class="k">let</span> <span class="nv">jsonSchema</span> <span class="o">=</span> <span class="kt">Schema</span><span class="o">.</span><span class="nf">array</span><span class="p">(</span>
      <span class="nv">items</span><span class="p">:</span> <span class="o">.</span><span class="nf">object</span><span class="p">(</span>
        <span class="nv">properties</span><span class="p">:</span> <span class="p">[</span>
          <span class="s">"name"</span><span class="p">:</span> <span class="o">.</span><span class="nf">string</span><span class="p">(),</span>
          <span class="s">"country"</span><span class="p">:</span> <span class="o">.</span><span class="nf">string</span><span class="p">()</span>
        <span class="p">]</span>
      <span class="p">)</span>
    <span class="p">)</span>

    <span class="kd">func</span> <span class="nf">generateTextContent</span><span class="p">(</span><span class="nv">prompt</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="k">async</span> <span class="k">throws</span> <span class="o">-&gt;</span> <span class="kt">String</span><span class="p">?</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">model</span> <span class="o">=</span> <span class="n">vertex</span><span class="o">.</span><span class="nf">generativeModel</span><span class="p">(</span>
            <span class="nv">modelName</span><span class="p">:</span> <span class="s">"gemini-1.5-flash"</span>
        <span class="p">)</span>

        <span class="k">return</span> <span class="k">try</span> <span class="k">await</span> <span class="n">model</span><span class="o">.</span><span class="nf">generateContent</span><span class="p">(</span><span class="n">prompt</span><span class="p">)</span><span class="o">.</span><span class="n">text</span>
    <span class="p">}</span>


    <span class="kd">func</span> <span class="nf">generateJsonContent</span><span class="p">(</span><span class="nv">prompt</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="k">async</span> <span class="k">throws</span> <span class="o">-&gt;</span> <span class="kt">String</span><span class="p">?</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">model</span> <span class="o">=</span> <span class="n">vertex</span><span class="o">.</span><span class="nf">generativeModel</span><span class="p">(</span>
            <span class="nv">modelName</span><span class="p">:</span> <span class="s">"gemini-1.5-flash"</span><span class="p">,</span>
            <span class="nv">generationConfig</span><span class="p">:</span> <span class="kt">GenerationConfig</span><span class="p">(</span>
                <span class="nv">responseMIMEType</span><span class="p">:</span> <span class="s">"application/json"</span><span class="p">,</span>
                <span class="nv">responseSchema</span><span class="p">:</span> <span class="n">jsonSchema</span>
            <span class="p">)</span>
        <span class="p">)</span>

        <span class="k">return</span> <span class="k">try</span> <span class="k">await</span> <span class="n">model</span><span class="o">.</span><span class="nf">generateContent</span><span class="p">(</span><span class="n">prompt</span><span class="p">)</span><span class="o">.</span><span class="n">text</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">generateImage</span><span class="p">(</span><span class="nv">prompt</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="k">async</span> <span class="k">throws</span> <span class="o">-&gt;</span> <span class="kt">KotlinByteArray</span><span class="p">?</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">model</span> <span class="o">=</span> <span class="n">vertex</span><span class="o">.</span><span class="nf">imagenModel</span><span class="p">(</span><span class="nv">modelName</span><span class="p">:</span> <span class="s">"imagen-3.0-generate-002"</span><span class="p">)</span>

        <span class="k">let</span> <span class="nv">response</span> <span class="o">=</span> <span class="k">try</span> <span class="k">await</span> <span class="n">model</span><span class="o">.</span><span class="nf">generateImages</span><span class="p">(</span><span class="nv">prompt</span><span class="p">:</span> <span class="n">prompt</span><span class="p">)</span>

        <span class="k">guard</span> <span class="k">let</span> <span class="nv">image</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="n">images</span><span class="o">.</span><span class="n">first</span> <span class="k">else</span> <span class="p">{</span>
          <span class="k">return</span> <span class="kc">nil</span>
        <span class="p">}</span>

        <span class="k">let</span> <span class="nv">imageData</span> <span class="o">=</span> <span class="n">image</span><span class="o">.</span><span class="n">data</span>
        <span class="k">return</span> <span class="n">imageData</span><span class="o">.</span><span class="nf">toByteArray</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>That iOS implementation of <code class="language-plaintext highlighter-rouge">GenerativeModel</code> is passed down to shared code when initialising Koin. Note that this is where we’re initialsing Firebase on iOS as well.</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="code"><pre><span class="kd">@main</span>
<span class="kd">struct</span> <span class="nv">iOSApp</span><span class="p">:</span> <span class="kt">App</span> <span class="p">{</span>
    <span class="nf">init</span><span class="p">()</span> <span class="p">{</span>
        <span class="kt">FirebaseApp</span><span class="o">.</span><span class="nf">configure</span><span class="p">()</span>
        <span class="nf">initialiseKoin</span><span class="p">(</span><span class="nv">generativeModel</span><span class="p">:</span> <span class="kt">GenerativeModelIOS</span><span class="o">.</span><span class="n">shared</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">Scene</span> <span class="p">{</span>
        <span class="kt">WindowGroup</span> <span class="p">{</span>
            <span class="kt">ContentView</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>That iOS specific instance of <code class="language-plaintext highlighter-rouge">GenerativeModel</code> is added to the Koin object graph and is what will be injected in to our shared view model when running on iOS.</p>

<h4 id="koinioskt">Koin.ios.kt</h4>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="code"><pre><span class="k">fun</span> <span class="nf">initialiseKoin</span><span class="p">(</span><span class="n">generativeModel</span><span class="p">:</span> <span class="nc">GenerativeModel</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">startKoin</span> <span class="p">{</span>
        <span class="nf">modules</span><span class="p">(</span>
            <span class="n">commonModule</span><span class="p">,</span>
            <span class="nf">module</span> <span class="p">{</span> <span class="n">single</span><span class="p">&lt;</span><span class="nc">GenerativeModel</span><span class="p">&gt;</span> <span class="p">{</span> <span class="n">generativeModel</span> <span class="p">}</span> <span class="p">}</span>
        <span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>The Android version of Koin initialisation in turn adds <code class="language-plaintext highlighter-rouge">GenerativeModelAndroid</code> to the object graph.</p>

<h4 id="koinandroidkt">Koin.android.kt</h4>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="code"><pre><span class="k">fun</span> <span class="nf">initialiseKoin</span><span class="p">()</span> <span class="p">{</span>
    <span class="nf">startKoin</span> <span class="p">{</span>
        <span class="nf">modules</span><span class="p">(</span>
            <span class="n">commonModule</span><span class="p">,</span>
            <span class="nf">module</span> <span class="p">{</span> <span class="n">single</span><span class="p">&lt;</span><span class="nc">GenerativeModel</span><span class="p">&gt;</span> <span class="p">{</span> <span class="nc">GenerativeModelAndroid</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span>
        <span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>

<h3 id="android-client-setup">Android client setup</h3>

<p>We initialise Firebase and Koin in our main Android application class.</p>
<h4 id="buildgradlekts-1">build.gradle.kts</h4>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="code"><pre><span class="kd">class</span> <span class="nc">VertexAIKMPApp</span> <span class="p">:</span> <span class="nc">Application</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">onCreate</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">super</span><span class="p">.</span><span class="nf">onCreate</span><span class="p">()</span>
        <span class="nc">FirebaseApp</span><span class="p">.</span><span class="nf">initializeApp</span><span class="p">(</span><span class="k">this</span><span class="p">)</span>
        <span class="nf">initialiseKoin</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>

<h3 id="shared-viewmodel">Shared ViewModel</h3>

<p>We invoke the above APIs in the following shared view model (we’re using the KMP Jetpack ViewModel library here). That includes the <code class="language-plaintext highlighter-rouge">generateContent</code> function which the Compose UI code calls with the text prompt and a flag indicating whether to generate a json response or not.  If <code class="language-plaintext highlighter-rouge">generateJson</code> is set we also parse the response and return the structured data to the UI code which renders as a basic list (as shown in the screenshots above).  We’re also invoking <code class="language-plaintext highlighter-rouge">generateImage</code> from here as well.`</p>

<h4 id="generativemodelviewmodelkt">GenerativeModelViewModel.kt</h4>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
</pre></td><td class="code"><pre><span class="kd">class</span> <span class="nc">GenerativeModelViewModel</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">generativeModel</span><span class="p">:</span> <span class="nc">GenerativeModel</span><span class="p">)</span> <span class="p">:</span> <span class="nc">ViewModel</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">uiState</span> <span class="p">=</span> <span class="nc">MutableStateFlow</span><span class="p">&lt;</span><span class="nc">GenerativeModelUIState</span><span class="p">&gt;(</span><span class="nc">GenerativeModelUIState</span><span class="p">.</span><span class="nc">Initial</span><span class="p">)</span>

    <span class="k">fun</span> <span class="nf">generateContent</span><span class="p">(</span><span class="n">prompt</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">generateJson</span><span class="p">:</span> <span class="nc">Boolean</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">uiState</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="nc">GenerativeModelUIState</span><span class="p">.</span><span class="nc">Loading</span>
        <span class="n">viewModelScope</span><span class="p">.</span><span class="nf">launch</span> <span class="p">{</span>
            <span class="k">try</span> <span class="p">{</span>
                <span class="n">uiState</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="k">if</span> <span class="p">(</span><span class="n">generateJson</span><span class="p">)</span> <span class="p">{</span>
                    <span class="kd">val</span> <span class="py">response</span> <span class="p">=</span> <span class="n">generativeModel</span><span class="p">.</span><span class="nf">generateJsonContent</span><span class="p">(</span><span class="n">prompt</span><span class="p">)</span>
                    <span class="k">if</span> <span class="p">(</span><span class="n">response</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
                        <span class="kd">val</span> <span class="py">entities</span> <span class="p">=</span> <span class="nc">Json</span><span class="p">.</span><span class="n">decodeFromString</span><span class="p">&lt;</span><span class="nc">List</span><span class="p">&lt;</span><span class="nc">Entity</span><span class="p">&gt;&gt;(</span><span class="n">response</span><span class="p">)</span>
                        <span class="nc">GenerativeModelUIState</span><span class="p">.</span><span class="nc">Success</span><span class="p">(</span><span class="n">entityContent</span> <span class="p">=</span> <span class="n">entities</span><span class="p">)</span>
                    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                        <span class="nc">GenerativeModelUIState</span><span class="p">.</span><span class="nc">Error</span><span class="p">(</span><span class="s">"Error generating content"</span><span class="p">)</span>
                    <span class="p">}</span>
                <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                    <span class="kd">val</span> <span class="py">response</span> <span class="p">=</span> <span class="n">generativeModel</span><span class="p">.</span><span class="nf">generateTextContent</span><span class="p">(</span><span class="n">prompt</span><span class="p">)</span>
                    <span class="nc">GenerativeModelUIState</span><span class="p">.</span><span class="nc">Success</span><span class="p">(</span><span class="n">textContent</span> <span class="p">=</span> <span class="n">response</span><span class="p">)</span>
                <span class="p">}</span>
            <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="nc">Exception</span><span class="p">)</span> <span class="p">{</span>
                <span class="nc">GenerativeModelUIState</span><span class="p">.</span><span class="nc">Error</span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="n">message</span> <span class="o">?:</span> <span class="s">"Error generating content"</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">generateImage</span><span class="p">(</span><span class="n">prompt</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">uiState</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="nc">GenerativeModelUIState</span><span class="p">.</span><span class="nc">Loading</span>
        <span class="n">viewModelScope</span><span class="p">.</span><span class="nf">launch</span> <span class="p">{</span>
            <span class="n">uiState</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="k">try</span> <span class="p">{</span>
                <span class="kd">val</span> <span class="py">imageData</span> <span class="p">=</span> <span class="n">generativeModel</span><span class="p">.</span><span class="nf">generateImage</span><span class="p">(</span><span class="n">prompt</span><span class="p">)</span>
                 <span class="nc">GenerativeModelUIState</span><span class="p">.</span><span class="nc">Success</span><span class="p">(</span><span class="n">imageData</span> <span class="p">=</span> <span class="n">imageData</span><span class="p">)</span>
            <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="nc">Exception</span><span class="p">)</span> <span class="p">{</span>
                <span class="nc">GenerativeModelUIState</span><span class="p">.</span><span class="nc">Error</span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="n">message</span> <span class="o">?:</span> <span class="s">"Error generating content"</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="shared-compose-multiplatform-ui-code">Shared Compose Multiplatform UI code</h3>

<p>Finally, the following is taken from the shared Compose UI code that’s used to render the response.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
</pre></td><td class="code"><pre><span class="k">is</span> <span class="nc">GenerativeModelUIState</span><span class="p">.</span><span class="nc">Success</span> <span class="p">-&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">uiState</span><span class="p">.</span><span class="n">entityContent</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
        <span class="nc">LazyColumn</span> <span class="p">{</span>
            <span class="nf">items</span><span class="p">(</span><span class="n">uiState</span><span class="p">.</span><span class="n">entityContent</span><span class="p">)</span> <span class="p">{</span> <span class="n">item</span> <span class="p">-&gt;</span>
                <span class="nc">ListItem</span><span class="p">(</span>
                    <span class="n">headlineContent</span> <span class="p">=</span> <span class="p">{</span> <span class="nc">Text</span><span class="p">(</span><span class="n">item</span><span class="p">.</span><span class="n">name</span><span class="p">)},</span>
                    <span class="n">supportingContent</span> <span class="p">=</span> <span class="p">{</span> <span class="nc">Text</span><span class="p">(</span><span class="n">item</span><span class="p">.</span><span class="n">country</span><span class="p">)</span> <span class="p">}</span>
                <span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">uiState</span><span class="p">.</span><span class="n">textContent</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
        <span class="nc">Markdown</span><span class="p">(</span><span class="n">uiState</span><span class="p">.</span><span class="n">textContent</span><span class="p">)</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">uiState</span><span class="p">.</span><span class="n">imageData</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
        <span class="nc">AsyncImage</span><span class="p">(</span>
            <span class="n">model</span> <span class="p">=</span> <span class="nc">ImageRequest</span>
                <span class="p">.</span><span class="nc">Builder</span><span class="p">(</span><span class="nc">LocalPlatformContext</span><span class="p">.</span><span class="n">current</span><span class="p">)</span>
                <span class="p">.</span><span class="nf">data</span><span class="p">(</span><span class="n">uiState</span><span class="p">.</span><span class="n">imageData</span><span class="p">)</span>
                <span class="p">.</span><span class="nf">build</span><span class="p">(),</span>
            <span class="n">contentDescription</span> <span class="p">=</span> <span class="n">prompt</span><span class="p">,</span>
            <span class="n">contentScale</span> <span class="p">=</span> <span class="nc">ContentScale</span><span class="p">.</span><span class="nc">Fit</span><span class="p">,</span>
            <span class="n">modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">fillMaxWidth</span><span class="p">()</span>
        <span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br />
Featured in <a href="https://androidweekly.net/issues/issue-647">Android Weekly #647</a> and <a href="https://mailchi.mp/kotlinweekly/kotlin-weekly-432">Kotlin Weekly Issue #432</a></p>

<p><br /></p>
<h3 id="related-tweet">Related tweet</h3>

<div class="jekyll-twitter-plugin"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Using Vertex AI in a Compose/Kotlin Multiplatform project <a href="https://t.co/k6P1cO5zly">https://t.co/k6P1cO5zly</a></p>&mdash; John O&#39;Reilly (@joreilly) <a href="https://twitter.com/joreilly/status/1850532711443628413?ref_src=twsrc%5Etfw">October 27, 2024</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</div>]]></content><author><name></name></author><summary type="html"><![CDATA[Update 2nd April 2025: Article now includes code for image generation and also cleaner approach for injecting iOS version of code using Koin.]]></summary></entry><entry><title type="html">Using Circuit with kotlin-inject in a Kotlin/Compose Multiplatform project</title><link href="https://johnoreilly.dev/posts/circuit-cmp-kmp/" rel="alternate" type="text/html" title="Using Circuit with kotlin-inject in a Kotlin/Compose Multiplatform project" /><published>2024-10-05T00:00:00+01:00</published><updated>2024-10-05T00:00:00+01:00</updated><id>https://johnoreilly.dev/posts/circuit-cmp-kmp</id><content type="html" xml:base="https://johnoreilly.dev/posts/circuit-cmp-kmp/"><![CDATA[<p>We’ve seen increasing use of the <a href="https://slackhq.github.io/circuit/">Circuit</a> framework in Kotlin/Compose Multiplatform projects and I thought it was time to update one of the samples I have to make use of it.  This article outlines some of the key changes made to use Circuit in the <a href="https://github.com/joreilly/BikeShare">BikeShare</a> sample.  That project had already been using the <a href="https://github.com/evant/kotlin-inject">kotlin-inject</a> DI framework (more about that <a href="https://johnoreilly.dev/posts/kotlin-inject-kmp/">here</a>) so we’ll also show how that can be used to configure the Circuit related components we’re using.  Note that this implementation is inspired by the excellent <a href="https://github.com/chrisbanes/tivi">Tivi</a> sample.</p>

<h2 id="circuit">Circuit</h2>

<p>Circuit is summarised <a href="https://slackhq.github.io/circuit/">in the official documentation</a> as <em>“a simple, lightweight, and extensible framework for building Kotlin applications that’s Compose from the ground up”</em>.   It strongly enables a unidirectional data flow (UDF) approach that is based on the following principles.</p>

<blockquote>
  <p>Circuit’s core components are its Presenter and Ui interfaces.</p>
  <ol>
    <li>A Presenter and a Ui cannot directly access each other. They can only communicate through state and event emissions.</li>
    <li>UIs are compose-first.</li>
    <li>Presenters are also compose-first. They do not emit Compose UI, but they do use the Compose runtime to manage and emit state.</li>
    <li>Both Presenter and Ui each have a single composable function.</li>
    <li>In most cases, Circuit automatically connects presenters and UIs.</li>
    <li>Presenter and Ui are both generic types, with generics to define the UiState types they communicate with.</li>
    <li>They are keyed by Screens. One runs a new Presenter/Ui pairing by requesting them with a given Screen that they understand.</li>
  </ol>
</blockquote>

<h2 id="implementation">Implementation</h2>

<p>As mentioned, what’s outlined in this article is based on changes made to the <a href="https://github.com/joreilly/BikeShare">BikeShare</a> KMP/CMP sample.  That project makes use of the <a href="https://api.citybik.es/v2/">CityBikes API</a> to show bike share networks and associated bike availability in different countries around the world. The following shows screenshots for the country, network, and station list screens in the Compose for Desktop client.</p>

<p><img src="/images/bikeshare_screenshots.png" alt="Bikeshare Screenshot" /></p>

<p>In the case of the country list for example we have the following key Circuit components</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">CountryListScreen</code></li>
  <li><code class="language-plaintext highlighter-rouge">CountryListPresenter</code> (and associated <code class="language-plaintext highlighter-rouge">CountryListPresenterFactory</code> factory)</li>
  <li><code class="language-plaintext highlighter-rouge">CountryListUi</code> (and associated <code class="language-plaintext highlighter-rouge">CountryListUiFactory</code>)</li>
</ul>

<p><br /></p>

<h6 id="countrylistscreen">CountryListScreen</h6>

<p>A <code class="language-plaintext highlighter-rouge">Screen</code>, as mentioned above, is used as the key for a particular Circuit Presenter/Ui pairing.  In this case here it’s also used to encapsulate the state that a presenter can emit to the Ui and the events that the Ui can send to the presenter.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="code"><pre><span class="nd">@Parcelize</span>
<span class="n">data</span> <span class="kd">object</span> <span class="nc">CountryListScreen</span> <span class="p">:</span> <span class="nc">Screen</span> <span class="p">{</span>
    <span class="kd">data class</span> <span class="nc">State</span><span class="p">(</span>
        <span class="kd">val</span> <span class="py">countryList</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Country</span><span class="p">&gt;,</span>
        <span class="kd">val</span> <span class="py">eventSink</span><span class="p">:</span> <span class="p">(</span><span class="nc">Event</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Unit</span>
    <span class="p">)</span> <span class="p">:</span> <span class="nc">CircuitUiState</span>

    <span class="k">sealed</span> <span class="kd">class</span> <span class="nc">Event</span> <span class="p">:</span> <span class="nc">CircuitUiEvent</span> <span class="p">{</span>
        <span class="kd">data class</span> <span class="nc">CountryClicked</span><span class="p">(</span><span class="kd">val</span> <span class="py">countryCode</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Event</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>
<h6 id="countrylistpresenterfactorycountrylistpresenter">CountryListPresenterFactory/CountryListPresenter</h6>

<p>The following is the factory for creating <code class="language-plaintext highlighter-rouge">CountryListPresenter</code> presenters, keyed as mentioned by <code class="language-plaintext highlighter-rouge">CountryListScreen</code>.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="code"><pre><span class="nd">@Inject</span>
<span class="kd">class</span> <span class="nc">CountryListPresenterFactory</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">presenterFactory</span><span class="p">:</span> <span class="p">(</span><span class="nc">Navigator</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">CountryListPresenter</span><span class="p">,</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">Presenter</span><span class="p">.</span><span class="nc">Factory</span> <span class="p">{</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">create</span><span class="p">(</span><span class="n">screen</span><span class="p">:</span> <span class="nc">Screen</span><span class="p">,</span> <span class="n">navigator</span><span class="p">:</span> <span class="nc">Navigator</span><span class="p">,</span> <span class="n">context</span><span class="p">:</span> <span class="nc">CircuitContext</span><span class="p">):</span> <span class="nc">Presenter</span><span class="p">&lt;</span><span class="err">*</span><span class="p">&gt;?</span> <span class="p">{</span>
        <span class="k">return</span> <span class="k">when</span> <span class="p">(</span><span class="n">screen</span><span class="p">)</span> <span class="p">{</span>
            <span class="nc">CountryListScreen</span> <span class="p">-&gt;</span> <span class="nf">presenterFactory</span><span class="p">(</span><span class="n">navigator</span><span class="p">)</span>
            <span class="k">else</span> <span class="p">-&gt;</span> <span class="k">null</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>And then this is the implementation for <code class="language-plaintext highlighter-rouge">CountryListPresenter</code>.  This includes a single composable function that’s used to create the state that’s emitted to our UI (note this is making use of Compose Runtime as opposed to Compose UI)…it also handles any events sent to it from the UI (e.g. <code class="language-plaintext highlighter-rouge">CountryClicked</code>).</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="code"><pre><span class="nd">@Inject</span>
<span class="kd">class</span> <span class="nc">CountryListPresenter</span><span class="p">(</span>
    <span class="nd">@Assisted</span> <span class="k">private</span> <span class="kd">val</span> <span class="py">navigator</span><span class="p">:</span> <span class="nc">Navigator</span><span class="p">,</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">cityBikesRepository</span><span class="p">:</span> <span class="nc">CityBikesRepository</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">Presenter</span><span class="p">&lt;</span><span class="nc">CountryListScreen</span><span class="p">.</span><span class="nc">State</span><span class="p">&gt;</span> <span class="p">{</span>
    <span class="nd">@Composable</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">present</span><span class="p">():</span> <span class="nc">CountryListScreen</span><span class="p">.</span><span class="nc">State</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">groupedNetworkList</span> <span class="k">by</span> <span class="n">cityBikesRepository</span><span class="p">.</span><span class="n">groupedNetworkList</span><span class="p">.</span><span class="nf">collectAsState</span><span class="p">()</span>
        <span class="kd">val</span> <span class="py">countryCodeList</span> <span class="p">=</span> <span class="n">groupedNetworkList</span><span class="p">.</span><span class="n">keys</span><span class="p">.</span><span class="nf">toList</span><span class="p">()</span>
        <span class="kd">val</span> <span class="py">countryList</span> <span class="p">=</span> <span class="n">countryCodeList</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">countryCode</span> <span class="p">-&gt;</span> <span class="nc">Country</span><span class="p">(</span><span class="n">countryCode</span><span class="p">,</span> <span class="nf">getCountryName</span><span class="p">(</span><span class="n">countryCode</span><span class="p">))</span> <span class="p">}</span>
            <span class="p">.</span><span class="nf">sortedBy</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">displayName</span> <span class="p">}</span>
        <span class="k">return</span> <span class="nc">CountryListScreen</span><span class="p">.</span><span class="nc">State</span><span class="p">(</span><span class="n">countryList</span><span class="p">)</span> <span class="p">{</span> <span class="n">event</span> <span class="p">-&gt;</span>
            <span class="k">when</span> <span class="p">(</span><span class="n">event</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">is</span> <span class="nc">CountryListScreen</span><span class="p">.</span><span class="nc">Event</span><span class="p">.</span><span class="nc">CountryClicked</span> <span class="p">-&gt;</span> <span class="n">navigator</span><span class="p">.</span><span class="nf">goTo</span><span class="p">(</span><span class="nc">NetworkListScreen</span><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="n">countryCode</span><span class="p">))</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>
<h6 id="countrylistui">CountryListUi</h6>

<p>Finally we have the UI component.  This gets notified of state emissions from the associated presenter (<code class="language-plaintext highlighter-rouge">CountryListPresenter</code>) and sends any UI events to that presenter (in this example indicating that a country was selected in the list).</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="code"><pre><span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">CountryListUi</span><span class="p">(</span><span class="n">state</span><span class="p">:</span> <span class="nc">CountryListScreen</span><span class="p">.</span><span class="nc">State</span><span class="p">,</span> <span class="n">modifier</span><span class="p">:</span> <span class="nc">Modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">)</span> <span class="p">{</span>
    <span class="nc">Scaffold</span><span class="p">(</span><span class="n">modifier</span> <span class="p">=</span> <span class="n">modifier</span><span class="p">,</span> <span class="n">topBar</span> <span class="p">=</span> <span class="p">{</span> <span class="nc">TopAppBar</span><span class="p">(</span><span class="n">title</span> <span class="p">=</span> <span class="p">{</span> <span class="nc">Text</span><span class="p">(</span><span class="s">"Countries"</span><span class="p">)</span> <span class="p">})</span> <span class="p">})</span> <span class="p">{</span> <span class="n">innerPadding</span> <span class="p">-&gt;</span>
        <span class="nc">LazyColumn</span><span class="p">(</span><span class="n">modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">padding</span><span class="p">(</span><span class="n">innerPadding</span><span class="p">))</span> <span class="p">{</span>
            <span class="nf">items</span><span class="p">(</span><span class="n">state</span><span class="p">.</span><span class="n">countryList</span><span class="p">)</span> <span class="p">{</span> <span class="n">country</span> <span class="p">-&gt;</span>
                <span class="nc">CountryView</span><span class="p">(</span><span class="n">country</span><span class="p">)</span> <span class="p">{</span>
                    <span class="n">state</span><span class="p">.</span><span class="nf">eventSink</span><span class="p">(</span><span class="nc">CountryListScreen</span><span class="p">.</span><span class="nc">Event</span><span class="p">.</span><span class="nc">CountryClicked</span><span class="p">(</span><span class="n">country</span><span class="p">.</span><span class="n">code</span><span class="p">))</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>
<h6 id="bikeshareapp">BikeShareApp</h6>

<p>We also have the following in our “root” composable that uses Circuit to launch that initial <code class="language-plaintext highlighter-rouge">CountryListScreen</code></p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="code"><pre><span class="k">typealias</span> <span class="nc">BikeShareApp</span> <span class="p">=</span> <span class="nd">@Composable</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span>

<span class="nd">@Inject</span>
<span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">BikeShareApp</span><span class="p">(</span><span class="n">circuit</span><span class="p">:</span> <span class="nc">Circuit</span><span class="p">)</span> <span class="p">{</span>
    <span class="nc">MaterialTheme</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">backStack</span> <span class="p">=</span> <span class="nf">rememberSaveableBackStack</span><span class="p">(</span><span class="n">root</span> <span class="p">=</span> <span class="nc">CountryListScreen</span><span class="p">)</span>
        <span class="kd">val</span> <span class="py">navigator</span> <span class="p">=</span> <span class="nf">rememberCircuitNavigator</span><span class="p">(</span><span class="n">backStack</span><span class="p">)</span> <span class="p">{}</span>

        <span class="nc">CircuitCompositionLocals</span><span class="p">(</span><span class="n">circuit</span><span class="p">)</span> <span class="p">{</span>
            <span class="nc">NavigableCircuitContent</span><span class="p">(</span><span class="n">navigator</span> <span class="p">=</span> <span class="n">navigator</span><span class="p">,</span> <span class="n">backStack</span> <span class="p">=</span> <span class="n">backStack</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>

<h3 id="kotlin-inject-configuration">kotlin-inject configuration</h3>

<p>The following then shows how these components are configured using the <a href="https://github.com/evant/kotlin-inject">kotlin-inject</a> DI framework (we’re also showing the setup here for the network and station list related components).  Note also use of that framework’s support for <a href="https://github.com/evant/kotlin-inject?tab=readme-ov-file#multi-bindings">multi-binding</a> to allow setting up the set of <em>presenter</em> and <em>ui</em> factories.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
</pre></td><td class="code"><pre><span class="k">abstract</span> <span class="kd">val</span> <span class="py">bikeShareApp</span><span class="p">:</span> <span class="nc">BikeShareApp</span>

<span class="nd">@IntoSet</span>
<span class="nd">@Provides</span>
<span class="k">fun</span> <span class="nf">bindCountryListPresenterFactory</span><span class="p">(</span><span class="n">factory</span><span class="p">:</span> <span class="nc">CountryListPresenterFactory</span><span class="p">):</span> <span class="nc">Presenter</span><span class="p">.</span><span class="nc">Factory</span> <span class="p">=</span> <span class="n">factory</span>

<span class="nd">@IntoSet</span>
<span class="nd">@Provides</span>
<span class="k">fun</span> <span class="nf">bindCountryListUiFactory</span><span class="p">(</span><span class="n">factory</span><span class="p">:</span> <span class="nc">CountryListUiFactory</span><span class="p">):</span> <span class="nc">Ui</span><span class="p">.</span><span class="nc">Factory</span> <span class="p">=</span> <span class="n">factory</span>

<span class="nd">@IntoSet</span>
<span class="nd">@Provides</span>
<span class="k">fun</span> <span class="nf">bindNetworkListPresenterFactory</span><span class="p">(</span><span class="n">factory</span><span class="p">:</span> <span class="nc">NetworkListPresenterFactory</span><span class="p">):</span> <span class="nc">Presenter</span><span class="p">.</span><span class="nc">Factory</span> <span class="p">=</span> <span class="n">factory</span>

<span class="nd">@IntoSet</span>
<span class="nd">@Provides</span>
<span class="k">fun</span> <span class="nf">bindNetworkListUiFactory</span><span class="p">(</span><span class="n">factory</span><span class="p">:</span> <span class="nc">NetworkListUiFactory</span><span class="p">):</span> <span class="nc">Ui</span><span class="p">.</span><span class="nc">Factory</span> <span class="p">=</span> <span class="n">factory</span>

<span class="nd">@IntoSet</span>
<span class="nd">@Provides</span>
<span class="k">fun</span> <span class="nf">bindStationListPresenterFactory</span><span class="p">(</span><span class="n">factory</span><span class="p">:</span> <span class="nc">StationListPresenterFactory</span><span class="p">):</span> <span class="nc">Presenter</span><span class="p">.</span><span class="nc">Factory</span> <span class="p">=</span> <span class="n">factory</span>

<span class="nd">@IntoSet</span>
<span class="nd">@Provides</span>
<span class="k">fun</span> <span class="nf">bindStationListUiFactory</span><span class="p">(</span><span class="n">factory</span><span class="p">:</span> <span class="nc">StationListUiFactory</span><span class="p">):</span> <span class="nc">Ui</span><span class="p">.</span><span class="nc">Factory</span> <span class="p">=</span> <span class="n">factory</span>

<span class="nd">@Provides</span>
<span class="k">fun</span> <span class="nf">provideCircuit</span><span class="p">(</span>
    <span class="n">uiFactories</span><span class="p">:</span> <span class="nc">Set</span><span class="p">&lt;</span><span class="nc">Ui</span><span class="p">.</span><span class="nc">Factory</span><span class="p">&gt;,</span>
    <span class="n">presenterFactories</span><span class="p">:</span> <span class="nc">Set</span><span class="p">&lt;</span><span class="nc">Presenter</span><span class="p">.</span><span class="nc">Factory</span><span class="p">&gt;</span>
<span class="p">):</span> <span class="nc">Circuit</span> <span class="p">=</span> <span class="nc">Circuit</span><span class="p">.</span><span class="nc">Builder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">addUiFactories</span><span class="p">(</span><span class="n">uiFactories</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">addPresenterFactories</span><span class="p">(</span><span class="n">presenterFactories</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Note that a recent release of Circuit added support for kotln-inject <a href="https://slackhq.github.io/circuit/code-gen/">code generation</a> that removes need for some of the boilerplate code shown here.  Hope to take a look soon at using that!</p>

<p><br />
Featured in <a href="https://androidweekly.net/issues/issue-643">Android Weekly #643</a></p>

<p><br /></p>
<h3 id="related-tweet">Related tweet</h3>

<div class="jekyll-twitter-plugin"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Using Circuit with kotlin-inject in a Kotlin/Compose Multiplatform project <a href="https://t.co/jncGH5HYDU">https://t.co/jncGH5HYDU</a><br /><br />A short article outlining some of changes made to the BikeShare <a href="https://twitter.com/hashtag/KMP?src=hash&amp;ref_src=twsrc%5Etfw">#KMP</a> sample to make of the really nice Circuit framework.</p>&mdash; John O&#39;Reilly (@joreilly) <a href="https://twitter.com/joreilly/status/1842561033191456785?ref_src=twsrc%5Etfw">October 5, 2024</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</div>]]></content><author><name></name></author><summary type="html"><![CDATA[We’ve seen increasing use of the Circuit framework in Kotlin/Compose Multiplatform projects and I thought it was time to update one of the samples I have to make use of it. This article outlines some of the key changes made to use Circuit in the BikeShare sample. That project had already been using the kotlin-inject DI framework (more about that here) so we’ll also show how that can be used to configure the Circuit related components we’re using. Note that this implementation is inspired by the excellent Tivi sample.]]></summary></entry><entry><title type="html">Using kotlin-inject in a Kotlin/Compose Multiplatform project</title><link href="https://johnoreilly.dev/posts/kotlin-inject-kmp/" rel="alternate" type="text/html" title="Using kotlin-inject in a Kotlin/Compose Multiplatform project" /><published>2024-07-27T00:00:00+01:00</published><updated>2024-07-27T00:00:00+01:00</updated><id>https://johnoreilly.dev/posts/kotlin-inject-kmp</id><content type="html" xml:base="https://johnoreilly.dev/posts/kotlin-inject-kmp/"><![CDATA[<p>I’ve been using <a href="https://github.com/InsertKoinIO/koin">Koin</a> in most of the Kotlin Multiplatform (KMP) samples I have but thought it would be good to include use of at least one other DI framework and this article outlines changes made to add <a href="https://github.com/evant/kotlin-inject">kotlin-inject</a> to the <a href="https://github.com/joreilly/BikeShare">BikeShare</a> KMP sample. This project retrieves data from a backend using <a href="https://github.com/ktorio/ktor">Ktor</a> and stores data locally on the device using <a href="https://github.com/realm/realm-kotlin">Realm</a>. It supports several platforms but we’ll be focusing on Android and iOS here.</p>

<h3 id="common-kmp-code">Common KMP code</h3>

<p>Starting off, the following were the changes made to <code class="language-plaintext highlighter-rouge">libs.version.toml</code> and <code class="language-plaintext highlighter-rouge">build.gradle.kts</code>.  Note also that we’re setting the <code class="language-plaintext highlighter-rouge">generateCompanionExtensions</code>option…this allows a more convenient way of creating the generated kotlin-inject components.</p>

<p>libs.version.toml</p>

<figure class="highlight"><pre><code class="language-toml" data-lang="toml"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="code"><pre><span class="py">kotlininject</span> <span class="p">=</span> <span class="s">"0.7.1"</span>


<span class="nn">kotlininject-compiler</span> <span class="o">=</span> <span class="p">{</span> <span class="py">module</span> <span class="p">=</span> <span class="s">"me.tatarka.inject:kotlin-inject-compiler-ksp"</span><span class="p">,</span> <span class="py">version.ref</span> <span class="p">=</span> <span class="s">"kotlininject"</span> <span class="p">}</span>
<span class="nn">kotlininject-runtime</span> <span class="o">=</span> <span class="p">{</span> <span class="py">module</span> <span class="p">=</span> <span class="s">"me.tatarka.inject:kotlin-inject-runtime"</span><span class="p">,</span> <span class="py">version.ref</span> <span class="p">=</span> <span class="s">"kotlininject"</span> <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>build.gradle.kts</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="code"><pre><span class="n">commonMain</span><span class="p">.</span><span class="nf">dependencies</span> <span class="p">{</span>
    <span class="o">..</span><span class="p">.</span>

    <span class="nf">implementation</span><span class="p">(</span><span class="n">libs</span><span class="p">.</span><span class="n">kotlininject</span><span class="p">.</span><span class="n">runtime</span><span class="p">)</span>
<span class="p">}</span>

<span class="o">..</span><span class="p">.</span>

<span class="nf">ksp</span> <span class="p">{</span>
    <span class="nf">arg</span><span class="p">(</span><span class="s">"me.tatarka.inject.generateCompanionExtensions"</span><span class="p">,</span> <span class="s">"true"</span><span class="p">)</span>
<span class="p">}</span>

<span class="nf">dependencies</span> <span class="p">{</span>
    <span class="nf">add</span><span class="p">(</span><span class="s">"kspAndroid"</span><span class="p">,</span> <span class="n">libs</span><span class="p">.</span><span class="n">kotlininject</span><span class="p">.</span><span class="n">compiler</span><span class="p">)</span>
    <span class="nf">add</span><span class="p">(</span><span class="s">"kspIosX64"</span><span class="p">,</span> <span class="n">libs</span><span class="p">.</span><span class="n">kotlininject</span><span class="p">.</span><span class="n">compiler</span><span class="p">)</span>
    <span class="nf">add</span><span class="p">(</span><span class="s">"kspIosArm64"</span><span class="p">,</span> <span class="n">libs</span><span class="p">.</span><span class="n">kotlininject</span><span class="p">.</span><span class="n">compiler</span><span class="p">)</span>
    <span class="nf">add</span><span class="p">(</span><span class="s">"kspIosSimulatorArm64"</span><span class="p">,</span> <span class="n">libs</span><span class="p">.</span><span class="n">kotlininject</span><span class="p">.</span><span class="n">compiler</span><span class="p">)</span>
    <span class="nf">add</span><span class="p">(</span><span class="s">"kspJvm"</span><span class="p">,</span> <span class="n">libs</span><span class="p">.</span><span class="n">kotlininject</span><span class="p">.</span><span class="n">compiler</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>We then define the shared dependencies in <code class="language-plaintext highlighter-rouge">SharedApplicationComponent.kt</code> as shown below. We’re using a single interface given the scope of this project but typically, in larger projects, the dependencies would get grouped into different interfaces.</p>

<p>As we’ll see later, we’re also going to create platform specific components that subclass this interface.  This will allow both creating platform specific versions of the dependencies (for example <code class="language-plaintext highlighter-rouge">getHttpClientEngine</code>which is used to create Ktor client engine for each platform) and also to allow clients to pass in objects that might be needed for each platform (though not doing that yet in this project).</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
</pre></td><td class="code"><pre><span class="kd">interface</span> <span class="nc">SharedApplicationComponent</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">countriesViewModel</span><span class="p">:</span> <span class="nc">CountriesViewModelShared</span>
    <span class="kd">val</span> <span class="py">networksViewModel</span><span class="p">:</span> <span class="nc">NetworksViewModelShared</span>
    <span class="kd">val</span> <span class="py">stationsViewModel</span><span class="p">:</span> <span class="nc">StationsViewModelShared</span>


    <span class="kd">val</span> <span class="py">repository</span><span class="p">:</span> <span class="nc">CityBikesRepository</span>
    <span class="kd">val</span> <span class="py">cityBikesApi</span><span class="p">:</span> <span class="nc">CityBikesApi</span>

    <span class="kd">val</span> <span class="py">json</span><span class="p">:</span> <span class="nc">Json</span>
        <span class="nd">@Provides</span> <span class="k">get</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Json</span> <span class="p">{</span> <span class="n">isLenient</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span> <span class="n">ignoreUnknownKeys</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span> <span class="n">useAlternativeNames</span> <span class="p">=</span> <span class="k">false</span> <span class="p">}</span>

    <span class="kd">val</span> <span class="py">realm</span><span class="p">:</span> <span class="nc">Realm</span>
        <span class="nd">@Provides</span> <span class="k">get</span><span class="p">()</span> <span class="p">{</span>
            <span class="kd">val</span> <span class="py">config</span> <span class="p">=</span> <span class="nc">RealmConfiguration</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="n">schema</span> <span class="p">=</span> <span class="nf">setOf</span><span class="p">(</span><span class="nc">NetworkDb</span><span class="o">::</span><span class="k">class</span><span class="p">))</span>
            <span class="k">return</span> <span class="nc">Realm</span><span class="p">.</span><span class="k">open</span><span class="p">(</span><span class="n">config</span><span class="p">)</span>
        <span class="p">}</span>

    <span class="nd">@Provides</span>
    <span class="k">fun</span> <span class="nf">getHttpClientEngine</span><span class="p">():</span> <span class="nc">HttpClientEngine</span>

    <span class="nd">@Provides</span>
    <span class="k">fun</span> <span class="nf">httpClient</span><span class="p">():</span> <span class="nc">HttpClient</span> <span class="p">=</span> <span class="nf">createHttpClient</span><span class="p">(</span><span class="nf">getHttpClientEngine</span><span class="p">(),</span> <span class="n">json</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">fun</span> <span class="nf">createHttpClient</span><span class="p">(</span><span class="n">httpClientEngine</span><span class="p">:</span> <span class="nc">HttpClientEngine</span><span class="p">,</span> <span class="n">json</span><span class="p">:</span> <span class="nc">Json</span><span class="p">)</span> <span class="p">=</span> <span class="nc">HttpClient</span><span class="p">(</span><span class="n">httpClientEngine</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">install</span><span class="p">(</span><span class="nc">ContentNegotiation</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">json</span><span class="p">(</span><span class="n">json</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="nf">install</span><span class="p">(</span><span class="nc">Logging</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">logger</span> <span class="p">=</span> <span class="nc">Logger</span><span class="p">.</span><span class="nc">DEFAULT</span>
        <span class="n">level</span> <span class="p">=</span> <span class="nc">LogLevel</span><span class="p">.</span><span class="nc">INFO</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>The way those dependencies get injected then in various classes is through use of <code class="language-plaintext highlighter-rouge">@Inject</code> annotation as shown here (we also mark in this case as <code class="language-plaintext highlighter-rouge">@Singleton</code>)</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="code"><pre><span class="nd">@Inject</span> <span class="nd">@Singleton</span>
<span class="kd">class</span> <span class="nc">CityBikesRepository</span><span class="p">(</span><span class="kd">val</span> <span class="py">cityBikesApi</span><span class="p">:</span> <span class="nc">CityBikesApi</span><span class="p">,</span><span class="kd">val</span>  <span class="py">realm</span><span class="p">:</span> <span class="nc">Realm</span><span class="p">)</span> <span class="p">{</span>
    <span class="o">..</span><span class="p">.</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>

<h3 id="android">Android</h3>

<p>The Android specific version of the kotlin-inject component (<code class="language-plaintext highlighter-rouge">AndroidApplicationComponent.kt</code>) is shown below (created in <code class="language-plaintext highlighter-rouge">androidMain</code> in the shared KMP module).  In this case we’re using that component to add dependencies for various Composable functions that we want to be passed particular dependencies.  We also define the Android specific version of the Ktor client engine here.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="code"><pre><span class="nd">@Component</span>
<span class="nd">@Singleton</span>
<span class="k">abstract</span> <span class="kd">class</span> <span class="nc">AndroidApplicationComponent</span><span class="p">:</span> <span class="nc">SharedApplicationComponent</span> <span class="p">{</span>
    <span class="k">abstract</span> <span class="kd">val</span> <span class="py">countryListScreen</span><span class="p">:</span> <span class="nc">CountryListScreen</span>
    <span class="k">abstract</span> <span class="kd">val</span> <span class="py">networkListScreen</span><span class="p">:</span> <span class="nc">NetworkListScreen</span>
    <span class="k">abstract</span> <span class="kd">val</span> <span class="py">stationsScreen</span><span class="p">:</span> <span class="nc">StationsScreen</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">getHttpClientEngine</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Android</span><span class="p">.</span><span class="nf">create</span><span class="p">()</span>

    <span class="k">companion</span> <span class="k">object</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>To allow dependencies to be passed to Composable functions we need to use approach shown below (more info on that in <a href="https://github.com/evant/kotlin-inject/blob/main/docs/android.md#compose">this page</a>). Note also the use of <code class="language-plaintext highlighter-rouge">@Assisted</code> for parameters that we’ll be using when those composable functions are used.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="code"><pre><span class="k">typealias</span> <span class="nc">CountryListScreen</span> <span class="p">=</span> <span class="nd">@Composable</span> <span class="p">((</span><span class="n">country</span><span class="p">:</span> <span class="nc">Country</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Unit</span>

<span class="nd">@Inject</span>
<span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">CountryListScreen</span><span class="p">(</span><span class="n">viewModel</span><span class="p">:</span> <span class="nc">CountriesViewModelShared</span><span class="p">,</span> <span class="nd">@Assisted</span> <span class="n">countrySelected</span><span class="p">:</span> <span class="p">(</span><span class="n">country</span><span class="p">:</span> <span class="nc">Country</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">countryList</span> <span class="k">by</span> <span class="n">viewModel</span><span class="p">.</span><span class="n">countryList</span><span class="p">.</span><span class="nf">collectAsState</span><span class="p">()</span>
    <span class="o">..</span><span class="p">.</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>The following are the changes needed then in the Android client module.  We firstly add code to create the component in <code class="language-plaintext highlighter-rouge">BikeShareApplication.kt</code></p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="code"><pre><span class="kd">class</span> <span class="nc">BikeShareApplication</span> <span class="p">:</span> <span class="nc">Application</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">component</span><span class="p">:</span> <span class="nc">AndroidApplicationComponent</span> <span class="k">by</span> <span class="nf">lazy</span> <span class="p">{</span>
        <span class="nc">AndroidApplicationComponent</span><span class="p">.</span><span class="nf">create</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="o">..</span><span class="p">.</span>    
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>That gets used then in <code class="language-plaintext highlighter-rouge">MainActivity.kt</code> as shown below.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="code"><pre><span class="kd">class</span> <span class="nc">MainActivity</span> <span class="p">:</span> <span class="nc">ComponentActivity</span><span class="p">()</span> <span class="p">{</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">onCreate</span><span class="p">(</span><span class="n">savedInstanceState</span><span class="p">:</span> <span class="nc">Bundle</span><span class="p">?)</span> <span class="p">{</span>
        <span class="o">..</span><span class="p">.</span>

        <span class="kd">val</span> <span class="py">applicationComponent</span> <span class="p">=</span> <span class="p">(</span><span class="n">applicationContext</span> <span class="k">as</span> <span class="nc">BikeShareApplication</span><span class="p">).</span><span class="n">component</span>

        <span class="nf">setContent</span> <span class="p">{</span>
            <span class="nc">BikeShareTheme</span> <span class="p">{</span>
                <span class="nc">BikeShareApp</span><span class="p">(</span><span class="n">applicationComponent</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>The android app is using Jetpack Navigation and this is an example of how the appropriate Composable function was retrieved from the component and used then in our <code class="language-plaintext highlighter-rouge">NavHost</code>.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="code"><pre><span class="nc">NavHost</span><span class="p">(</span><span class="n">navController</span><span class="p">,</span> <span class="n">startDestination</span> <span class="p">=</span> <span class="nc">Screen</span><span class="p">.</span><span class="nc">CountryListScreen</span><span class="p">.</span><span class="n">title</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">countryListScreen</span> <span class="p">=</span> <span class="n">applicationComponent</span><span class="p">.</span><span class="n">countryListScreen</span>
    <span class="nf">composable</span><span class="p">(</span><span class="nc">Screen</span><span class="p">.</span><span class="nc">CountryListScreen</span><span class="p">.</span><span class="n">title</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">countryListScreen</span> <span class="p">{</span>
            <span class="n">navController</span><span class="p">.</span><span class="nf">navigate</span><span class="p">(</span><span class="nc">Screen</span><span class="p">.</span><span class="nc">NetworkListScreen</span><span class="p">.</span><span class="n">title</span> <span class="p">+</span> <span class="s">"/${it.code}"</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="o">..</span><span class="p">.</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>

<h3 id="ios">iOS</h3>

<p>For iOS we also have a platform specific component (<code class="language-plaintext highlighter-rouge">IosApplicationComponent</code> as shown below) which is created in <code class="language-plaintext highlighter-rouge">iosMain</code> in the shared KMP module.  As with Android, we’re also creating the platform specific version of the Ktor client engine here.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="code"><pre><span class="nd">@Component</span>
<span class="nd">@Singleton</span>
<span class="k">abstract</span> <span class="kd">class</span> <span class="nc">IosApplicationComponent</span><span class="p">:</span> <span class="nc">SharedApplicationComponent</span> <span class="p">{</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">getHttpClientEngine</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Darwin</span><span class="p">.</span><span class="nf">create</span><span class="p">()</span>

    <span class="k">companion</span> <span class="k">object</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>In our SwiftUI client we create that component on startup and pass to <code class="language-plaintext highlighter-rouge">ContentView</code>.</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
</pre></td><td class="code"><pre><span class="k">let</span> <span class="nv">applicationCompoonent</span> <span class="o">=</span> <span class="kt">IosApplicationComponent</span><span class="o">.</span><span class="n">companion</span><span class="o">.</span><span class="nf">create</span><span class="p">()</span>
<span class="k">let</span> <span class="nv">contentView</span> <span class="o">=</span> <span class="kt">ContentView</span><span class="p">(</span><span class="nv">applicationCompoonent</span><span class="p">:</span> <span class="n">applicationCompoonent</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>The following then shows example in <code class="language-plaintext highlighter-rouge">ContentView</code> of how we’re using that component to look up one of the shared view models (done in this way also to allow <code class="language-plaintext highlighter-rouge">StateFlow</code>s in the shared view models to be observed in SwiftUI….using <a href="https://github.com/rickclephas/KMP-ObservableViewModel">KMP-ObservableViewModel</a> library).</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="code"><pre><span class="kd">struct</span> <span class="kt">ContentView</span> <span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">applicationCompoonent</span><span class="p">:</span> <span class="kt">IosApplicationComponent</span>
    <span class="kd">@ObservedViewModel</span> <span class="k">var</span> <span class="nv">viewModel</span><span class="p">:</span> <span class="kt">CountriesViewModelShared</span>
    
    <span class="nf">init</span><span class="p">(</span><span class="nv">applicationCompoonent</span><span class="p">:</span> <span class="kt">IosApplicationComponent</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">applicationCompoonent</span> <span class="o">=</span> <span class="n">applicationCompoonent</span>
        <span class="k">self</span><span class="o">.</span><span class="n">viewModel</span> <span class="o">=</span> <span class="n">applicationCompoonent</span><span class="o">.</span><span class="n">countriesViewModel</span>
    <span class="p">}</span>
    
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="o">...</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br />
Featured in <a href="https://androidweekly.net/issues/issue-633">Android Weekly #633</a> and <a href="https://mailchi.mp/kotlinweekly/kotlin-weekly-418">Kotlin Weekly Issue #418</a></p>

<p><br /></p>
<h3 id="related-tweet">Related tweet</h3>

<div class="jekyll-twitter-plugin"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Wrote a short article outlining changes made to add kotlin-inject to the BikeShare Kotlin Multiplatform sample. <a href="https://t.co/44VDe6X9t9">https://t.co/44VDe6X9t9</a> <a href="https://twitter.com/hashtag/KMP?src=hash&amp;ref_src=twsrc%5Etfw">#KMP</a></p>&mdash; John O&#39;Reilly (@joreilly) <a href="https://twitter.com/joreilly/status/1817224465001947486?ref_src=twsrc%5Etfw">July 27, 2024</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</div>]]></content><author><name></name></author><summary type="html"><![CDATA[I’ve been using Koin in most of the Kotlin Multiplatform (KMP) samples I have but thought it would be good to include use of at least one other DI framework and this article outlines changes made to add kotlin-inject to the BikeShare KMP sample. This project retrieves data from a backend using Ktor and stores data locally on the device using Realm. It supports several platforms but we’ll be focusing on Android and iOS here.]]></summary></entry><entry><title type="html">Exploring New Worlds of UI sharing possibilities in PeopleInSpace using Compose Multiplatform</title><link href="https://johnoreilly.dev/posts/exploring-compose_multiplatform_sharing_ios/" rel="alternate" type="text/html" title="Exploring New Worlds of UI sharing possibilities in PeopleInSpace using Compose Multiplatform" /><published>2024-06-30T00:00:00+01:00</published><updated>2024-06-30T00:00:00+01:00</updated><id>https://johnoreilly.dev/posts/exploring-compose_multiplatform_sharing_ios</id><content type="html" xml:base="https://johnoreilly.dev/posts/exploring-compose_multiplatform_sharing_ios/"><![CDATA[<p>I’ve written about the use of Compose Multiplatform for sharing UI code in a number of previous articles but was
inspired by following recent session by Touchlab (and related <a href="https://github.com/touchlab/compose-swiftui-codelab">sample</a>)
to explore this further….in particular in the
<a href="https://github.com/joreilly/PeopleInSpace">PeopleInSpace</a> Kotlin Multiplatform (KMP) sample and specifically for the UI used for showing the position
of the International Space Station (ISS).</p>

<div class="jekyll-twitter-plugin"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Touchlab:🌴Summer Streamin&#39; Series 🏖️<br /><br />🌟 Multiplatform Compose + SwiftUI = The Native App Future<br />📅 Livestream - Friday, June 28th 2024<br />🔊 <a href="https://twitter.com/faogustavo?ref_src=twsrc%5Etfw">@faogustavo</a>, <a href="https://twitter.com/DevSrSouza?ref_src=twsrc%5Etfw">@DevSrSouza</a> &amp; <a href="https://twitter.com/kpgalligan?ref_src=twsrc%5Etfw">@kpgalligan</a> <br />🔂 <a href="https://twitter.com/hashtag/KotlinConf?src=hash&amp;ref_src=twsrc%5Etfw">#KotlinConf</a>&#39;24 CodeLab <br /> <a href="https://t.co/2nRQNurBfV">https://t.co/2nRQNurBfV</a></p>&mdash; Touchlab (@TouchlabHQ) <a href="https://twitter.com/TouchlabHQ/status/1805329791442141341?ref_src=twsrc%5Etfw">June 24, 2024</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</div>

<h3 id="overview">Overview</h3>

<p><a href="https://github.com/joreilly/PeopleInSpace">PeopleInSpace</a> is a basic Kotlin Multiplatform sample that shows
the list of people in space along with the position of the ISS…and it’s the latter that we’ll be focussing
on here. This project had previously only used SwiftUI for the iOS UI (along with Compose on Android) but
with the changes outlined here it’s now using the following for the ISS Position screen shown below.</p>

<ul>
  <li>SwiftUI for the “outer shell” of the screen including the navigation and bottom bars</li>
  <li>Shared Compose Multiplatform UI code the content area</li>
  <li>Native components for the map (standard MapKit on iOS and <a href="https://github.com/osmdroid/osmdroid">osmdroid</a> on Android)</li>
</ul>

<p>This is perhaps somewhat contrived for this basic example but wanted to explore how this pattern of UI reuse
could work as I believe it’s one that many apps might benefit from.</p>

<p><img src="/images/peopleinspace_iss_screenshots.png" alt="ISS screenshot on Android and iOS" /></p>

<p><br /></p>

<h3 id="implementation">Implementation</h3>

<p>We’ll now outline changes needed to allow this arrangement. Taking the iOS client as an example the following
shows the areas we described above.</p>
<ul>
  <li><span style="color:blue">Blue</span>: outer SwiftUI shell (<em>ISSPositionScreen.swift</em>)</li>
  <li><span style="color:green">Green</span>: shared Compose Multiplatform UI code for the content area (<em>ISSPositionContent.kt</em>)</li>
  <li><span style="color:red">Red</span>: SwiftUI map component (<em>NativeISSMapView.swift</em>)</li>
</ul>

<p><img src="/images/iss_screen.png" alt="ISS screenshot iOS" /></p>

<h4 id="outer-swiftui-shell">Outer SwiftUI shell</h4>

<p>We’re using in this case a standard SwiftUI <code class="language-plaintext highlighter-rouge">TabView</code> to display the navigation bar shown at the bottom of the screen.</p>

<h6 id="contentviewswift">ContentView.swift</h6>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="code"><pre><span class="kt">TabView</span> <span class="p">{</span>
    <span class="kt">PeopleListScreen</span><span class="p">()</span>
        <span class="o">.</span><span class="n">tabItem</span> <span class="p">{</span>
            <span class="kt">Label</span><span class="p">(</span><span class="s">"People"</span><span class="p">,</span> <span class="nv">systemImage</span><span class="p">:</span> <span class="s">"person"</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="kt">ISSPositionScreen</span><span class="p">()</span>
        <span class="o">.</span><span class="n">tabItem</span> <span class="p">{</span>
            <span class="kt">Label</span><span class="p">(</span><span class="s">"ISS Position"</span><span class="p">,</span> <span class="nv">systemImage</span><span class="p">:</span> <span class="s">"location"</span><span class="p">)</span>
        <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>And the following is the implementation of that <code class="language-plaintext highlighter-rouge">ISSPositionScreen</code>. Note that we’re making use of a shared Kotlin
view model (<code class="language-plaintext highlighter-rouge">ISSPositionViewModel</code>) and that will be passed
down through various levels so that the data exposed from it (the current position of the ISS) can be observed in each component.
Note that we’re also using SwiftUI’s navigation bar for this screen.</p>

<h6 id="isspositionscreenswift">ISSPositionScreen.swift</h6>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="code"><pre><span class="kd">struct</span> <span class="kt">ISSPositionScreen</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="k">var</span> <span class="nv">viewModel</span> <span class="o">=</span> <span class="kt">ISSPositionViewModel</span><span class="p">()</span>
        
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">NavigationView</span> <span class="p">{</span>
            <span class="kt">VStack</span> <span class="p">{</span>
                <span class="kt">ISSPositionContentViewController</span><span class="p">(</span><span class="nv">viewModel</span><span class="p">:</span> <span class="n">viewModel</span><span class="p">)</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">navigationBarTitle</span><span class="p">(</span><span class="kt">Text</span><span class="p">(</span><span class="s">"ISS Position"</span><span class="p">))</span>
            <span class="o">.</span><span class="nf">navigationBarTitleDisplayMode</span><span class="p">(</span><span class="o">.</span><span class="n">inline</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>That in turn consumes our shared Compose Multiplatform code by using the following <code class="language-plaintext highlighter-rouge">UIViewControllerRepresentable</code> implementation
to wrap that code.</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="code"><pre><span class="kd">struct</span> <span class="kt">ISSPositionContentViewController</span><span class="p">:</span> <span class="kt">UIViewControllerRepresentable</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">viewModel</span><span class="p">:</span> <span class="kt">ISSPositionViewModel</span>
    
    <span class="kd">func</span> <span class="nf">makeUIViewController</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">UIViewController</span> <span class="p">{</span>
        <span class="kt">SharedViewControllers</span><span class="p">()</span><span class="o">.</span><span class="kt">ISSPositionContentViewController</span><span class="p">(</span>
            <span class="nv">viewModel</span><span class="p">:</span> <span class="n">viewModel</span><span class="p">,</span>
            <span class="nv">nativeViewFactory</span><span class="p">:</span> <span class="n">iOSNativeViewFactory</span><span class="o">.</span><span class="n">shared</span>
        <span class="p">)</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">updateUIViewController</span><span class="p">(</span><span class="n">_</span> <span class="nv">uiViewController</span><span class="p">:</span> <span class="kt">UIViewController</span><span class="p">,</span> <span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="p">{</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>We also importantly pass down following <code class="language-plaintext highlighter-rouge">NativeViewFactory</code> implementation which will allow that shared Compose code in turn
to include our SwiftUI map component.  Note that this approach is heavily based on the <a href="https://github.com/touchlab/compose-swiftui-codelab">Touchlab sample </a>
mentioned earlier.</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="code"><pre><span class="kd">class</span> <span class="nv">iOSNativeViewFactory</span> <span class="p">:</span> <span class="kt">NativeViewFactory</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="k">var</span> <span class="nv">shared</span> <span class="o">=</span> <span class="nf">iOSNativeViewFactory</span><span class="p">()</span>

    <span class="kd">func</span> <span class="nf">createISSMapView</span><span class="p">(</span><span class="nv">viewModel</span><span class="p">:</span> <span class="kt">ISSPositionViewModel</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">UIViewController</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">mapView</span> <span class="o">=</span> <span class="kt">NativeISSMapView</span><span class="p">(</span><span class="nv">viewModel</span><span class="p">:</span> <span class="n">viewModel</span><span class="p">)</span>
        <span class="k">return</span> <span class="kt">UIHostingController</span><span class="p">(</span><span class="nv">rootView</span><span class="p">:</span> <span class="n">mapView</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>

<h4 id="shared-compose-multiplatform-code">Shared Compose Multiplatform code</h4>

<p>The following is the implementation of <code class="language-plaintext highlighter-rouge">ISSPositionContentViewController</code> in shared KMP code.  It uses Compose’s
<code class="language-plaintext highlighter-rouge">ComposeUIViewController</code> to wrap <code class="language-plaintext highlighter-rouge">ISSPositionContent</code> which is the Composable that shows the content area of our screen.</p>

<h6 id="iosmainsharedviewcontrollerskt">iOSMain/SharedViewControllers.kt</h6>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="code"><pre><span class="kd">object</span> <span class="nc">SharedViewControllers</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">ISSPositionContentViewController</span><span class="p">(</span><span class="n">viewModel</span><span class="p">:</span> <span class="nc">ISSPositionViewModel</span><span class="p">,</span> <span class="n">nativeViewFactory</span><span class="p">:</span> <span class="nc">NativeViewFactory</span><span class="p">)</span> <span class="p">=</span> <span class="nc">ComposeUIViewController</span> <span class="p">{</span>
        <span class="nc">CompositionLocalProvider</span><span class="p">(</span><span class="nc">LocalNativeViewFactory</span> <span class="n">provides</span> <span class="n">nativeViewFactory</span><span class="p">)</span> <span class="p">{</span>
            <span class="nc">ISSPositionContent</span><span class="p">(</span><span class="n">viewModel</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>And shown below is that <code class="language-plaintext highlighter-rouge">ISSPositionContent</code> function (this is used on both iOS and Android).
It observes the ISS position (from the <code class="language-plaintext highlighter-rouge">StateFlow</code> in <code class="language-plaintext highlighter-rouge">ISSPositionViewModel</code>), updates some text to show
that position and then invokes <code class="language-plaintext highlighter-rouge">ISSMapView</code> (passing in the view model).</p>

<p>Note also use of <code class="language-plaintext highlighter-rouge">collectAsStateWithLifecycle</code> from JetBrain’s KMP version of the Jetpack Lifecycle library.  You
can see the effect of this by looking at logs in Xcode and noting that the ISS position polling stops when
you navigate to say different tab in the iOS client.</p>

<h6 id="isspositioncontentkt">ISSPositionContent.kt</h6>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="code"><pre><span class="nd">@Composable</span>
<span class="n">expect</span> <span class="k">fun</span> <span class="nf">ISSMapView</span><span class="p">(</span><span class="n">modifier</span><span class="p">:</span> <span class="nc">Modifier</span><span class="p">,</span> <span class="n">viewModel</span><span class="p">:</span> <span class="nc">ISSPositionViewModel</span><span class="p">)</span>

<span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">ISSPositionContent</span><span class="p">(</span><span class="n">viewModel</span><span class="p">:</span> <span class="nc">ISSPositionViewModel</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">position</span> <span class="k">by</span> <span class="n">viewModel</span><span class="p">.</span><span class="n">position</span><span class="p">.</span><span class="nf">collectAsStateWithLifecycle</span><span class="p">()</span>

    <span class="nc">Column</span> <span class="p">{</span>
        <span class="nc">Column</span><span class="p">(</span><span class="nc">Modifier</span><span class="p">.</span><span class="nf">fillMaxWidth</span><span class="p">(),</span> <span class="n">horizontalAlignment</span> <span class="p">=</span> <span class="nc">Alignment</span><span class="p">.</span><span class="nc">CenterHorizontally</span><span class="p">)</span> <span class="p">{</span>
            <span class="nc">Text</span><span class="p">(</span><span class="n">text</span> <span class="p">=</span> <span class="s">"Latitude = ${position.latitude}"</span><span class="p">)</span>
            <span class="nc">Text</span><span class="p">(</span><span class="n">text</span> <span class="p">=</span> <span class="s">"Longitude = ${position.longitude}"</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="nc">Spacer</span><span class="p">(</span><span class="nc">Modifier</span><span class="p">.</span><span class="nf">height</span><span class="p">(</span><span class="mi">16</span><span class="p">.</span><span class="n">dp</span><span class="p">))</span>
        <span class="nc">ISSMapView</span><span class="p">(</span><span class="nc">Modifier</span><span class="p">.</span><span class="nf">fillMaxHeight</span><span class="p">().</span><span class="nf">fillMaxWidth</span><span class="p">(),</span> <span class="n">viewModel</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>We use Kotlin Multiplatform’s expect/actual mechanism to define implementations of <code class="language-plaintext highlighter-rouge">ISSMapView</code> for each
platform.  The following shows that implementation for iOS (there’s also an implementation for Android that uses
the osmandroid map library).</p>

<p>Here we make use of Compose’s <code class="language-plaintext highlighter-rouge">UIKitViewController</code> (and <code class="language-plaintext highlighter-rouge">LocalNativeViewFactory</code> we passed in earlier) to
include our SwiftUI based component that shows the map.</p>

<h6 id="iosmainissmapviewioskt">iOSMain/ISSMapView.ios.kt</h6>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="code"><pre><span class="nd">@Composable</span>
<span class="n">actual</span> <span class="k">fun</span> <span class="nf">ISSMapView</span><span class="p">(</span><span class="n">modifier</span><span class="p">:</span> <span class="nc">Modifier</span><span class="p">,</span> <span class="n">viewModel</span><span class="p">:</span> <span class="nc">ISSPositionViewModel</span><span class="p">)</span> <span class="p">{</span>
    <span class="nc">MapKitView</span><span class="p">(</span>
        <span class="n">modifier</span> <span class="p">=</span> <span class="n">modifier</span><span class="p">,</span>
        <span class="n">viewModel</span> <span class="p">=</span> <span class="n">viewModel</span><span class="p">,</span>
    <span class="p">)</span>
<span class="p">}</span>

<span class="nd">@Composable</span>
<span class="k">internal</span> <span class="k">fun</span> <span class="nf">MapKitView</span><span class="p">(</span>
    <span class="n">modifier</span><span class="p">:</span> <span class="nc">Modifier</span><span class="p">,</span>
    <span class="n">viewModel</span><span class="p">:</span> <span class="nc">ISSPositionViewModel</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">factory</span> <span class="p">=</span> <span class="nc">LocalNativeViewFactory</span><span class="p">.</span><span class="n">current</span>

    <span class="nc">UIKitViewController</span><span class="p">(</span>
        <span class="n">modifier</span> <span class="p">=</span> <span class="n">modifier</span><span class="p">,</span>
        <span class="n">factory</span> <span class="p">=</span> <span class="p">{</span>
            <span class="n">factory</span><span class="p">.</span><span class="nf">createISSMapView</span><span class="p">(</span><span class="n">viewModel</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>

<h4 id="swiftui-map-component">SwiftUI map component</h4>

<p>And this is that SwiftUI component to display the map (using MapKit) and show live updates of the ISS position.  It does this
by observing <code class="language-plaintext highlighter-rouge">viewModel.position</code> (from the shared Kotlin view model) using the <a href="https://skie.touchlab.co/features/flows-in-swiftui">SKIE</a> library.</p>

<h6 id="nativeissmapviewswift">NativeISSMapView.swift</h6>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="code"><pre><span class="kd">struct</span> <span class="kt">NativeISSMapView</span> <span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">viewModel</span><span class="p">:</span> <span class="kt">ISSPositionViewModel</span>
    
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">VStack</span> <span class="p">{</span>
            <span class="kt">Observing</span><span class="p">(</span><span class="n">viewModel</span><span class="o">.</span><span class="n">position</span><span class="p">)</span> <span class="p">{</span> <span class="n">issPosition</span> <span class="k">in</span>
                <span class="k">let</span> <span class="nv">issCoordinatePosition</span> <span class="o">=</span> <span class="kt">CLLocationCoordinate2D</span><span class="p">(</span><span class="nv">latitude</span><span class="p">:</span> <span class="n">issPosition</span><span class="o">.</span><span class="n">latitude</span><span class="p">,</span> <span class="nv">longitude</span><span class="p">:</span> <span class="n">issPosition</span><span class="o">.</span><span class="n">longitude</span><span class="p">)</span>
                <span class="k">let</span> <span class="nv">regionBinding</span> <span class="o">=</span> <span class="kt">Binding</span><span class="o">&lt;</span><span class="kt">MKCoordinateRegion</span><span class="o">&gt;</span><span class="p">(</span>
                    <span class="nv">get</span><span class="p">:</span> <span class="p">{</span>
                        <span class="kt">MKCoordinateRegion</span><span class="p">(</span><span class="nv">center</span><span class="p">:</span> <span class="n">issCoordinatePosition</span><span class="p">,</span> <span class="nv">span</span><span class="p">:</span> <span class="kt">MKCoordinateSpan</span><span class="p">(</span><span class="nv">latitudeDelta</span><span class="p">:</span> <span class="mi">150</span><span class="p">,</span> <span class="nv">longitudeDelta</span><span class="p">:</span> <span class="mi">150</span><span class="p">))</span>
                    <span class="p">},</span>
                    <span class="nv">set</span><span class="p">:</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span> <span class="p">}</span>
                <span class="p">)</span>

                <span class="kt">Map</span><span class="p">(</span><span class="nv">coordinateRegion</span><span class="p">:</span> <span class="n">regionBinding</span><span class="p">,</span> <span class="nv">showsUserLocation</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
                    <span class="nv">annotationItems</span><span class="p">:</span> <span class="p">[</span> <span class="kt">Location</span><span class="p">(</span><span class="nv">coordinate</span><span class="p">:</span> <span class="n">issCoordinatePosition</span><span class="p">)</span> <span class="p">])</span> <span class="p">{</span> <span class="p">(</span><span class="n">location</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">MapPin</span> <span class="k">in</span>
                    <span class="kt">MapPin</span><span class="p">(</span><span class="nv">coordinate</span><span class="p">:</span> <span class="n">location</span><span class="o">.</span><span class="n">coordinate</span><span class="p">)</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br />
Featured in <a href="https://mailchi.mp/kotlinweekly/kotlin-weekly-414">Kotlin Weekly Issue #414</a> and <a href="https://androidweekly.net/issues/issue-630">Android Weekly #630</a></p>

<p><br /></p>
<h3 id="related-tweet">Related tweet</h3>

<div class="jekyll-twitter-plugin"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Exploring New Worlds of UI sharing possibilities in PeopleInSpace using Compose Multiplatform <a href="https://t.co/5fPnIial0V">https://t.co/5fPnIial0V</a><br /><br />Finally updated PeopleInSpace to include example of use of Compose Multiplatform and this post outlines changes made (heavily inspired by <a href="https://twitter.com/TouchlabHQ?ref_src=twsrc%5Etfw">@TouchlabHQ</a>&#39;s sample)</p>&mdash; John O&#39;Reilly (@joreilly) <a href="https://twitter.com/joreilly/status/1807428379169206395?ref_src=twsrc%5Etfw">June 30, 2024</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</div>]]></content><author><name></name></author><summary type="html"><![CDATA[I’ve written about the use of Compose Multiplatform for sharing UI code in a number of previous articles but was inspired by following recent session by Touchlab (and related sample) to explore this further….in particular in the PeopleInSpace Kotlin Multiplatform (KMP) sample and specifically for the UI used for showing the position of the International Space Station (ISS).]]></summary></entry><entry><title type="html">Consuming Jetpack Paging KMP code in SwiftUI and Compose clients</title><link href="https://johnoreilly.dev/posts/jetpack_paging_kmp/" rel="alternate" type="text/html" title="Consuming Jetpack Paging KMP code in SwiftUI and Compose clients" /><published>2024-05-17T00:00:00+01:00</published><updated>2024-05-17T00:00:00+01:00</updated><id>https://johnoreilly.dev/posts/jetpack_paging_kmp</id><content type="html" xml:base="https://johnoreilly.dev/posts/jetpack_paging_kmp/"><![CDATA[<p>This week saw the first Kotlin Multiplatform (KMP) stable release of the <a href="https://developer.android.com/jetpack/androidx/releases/paging#version_33_2">Jetpack Paging library</a>
(following on from a flurry of Jetpack KMP related announcements recently!).  In this article I’m going to outline changes made to add use
of that library to the <a href="https://github.com/joreilly/MortyComposeKMM">Morty</a> KMP sample and in particular show how that could
be consumed in the associated SwiftUI client.  We’ll also cover the Android Compose code for completeness but that works
as it did before when using the previously Android only version of that library.</p>

<div class="jekyll-twitter-plugin"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Wow, lots of AI and Gemini at <a href="https://twitter.com/hashtag/GoogleIO?src=hash&amp;ref_src=twsrc%5Etfw">#GoogleIO</a> today, but we couldn&#39;t pass up the opportunity to also include some <a href="https://twitter.com/hashtag/JetpackReleaseNotes?src=hash&amp;ref_src=twsrc%5Etfw">#JetpackReleaseNotes</a> with Lifecycle 2.8.0 and Paging 3.3.0 having their first KMP stable releases! Plus, ViewPager2 1.1.0 and Fragment 1.7.1!<a href="https://t.co/vyCuwEI9vC">https://t.co/vyCuwEI9vC</a></p>&mdash; Ian Lake (@ianhlake) <a href="https://twitter.com/ianhlake/status/1790599625508102541?ref_src=twsrc%5Etfw">May 15, 2024</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</div>

<p><br /></p>

<h3 id="project-overview">Project overview</h3>

<p><a href="https://github.com/joreilly/MortyComposeKMM">Morty</a> is a Kotlin Multiplatform sample that demonstrates use of GraphQL in shared KMP code using
the <a href="https://github.com/apollographql/apollo-kotlin">Apollo Kotlin</a> library and includes Jetpack Compose and SwiftUI clients (based on <a href="https://github.com/Dimillian/MortyUI">https://github.com/Dimillian/MortyUI</a> SwiftUI project).
The following are screenshots of the Android and iOS clients.</p>

<p><img src="/images/morty_screenshots.png" alt="Screenshots" /></p>

<p><br /></p>

<h3 id="updates-to-shared-kmp-code">Updates to shared KMP code</h3>

<p>The initial changes involved just moving the Jetpack Paging common dependency and existing <code class="language-plaintext highlighter-rouge">PagingSource</code> classes from the
Android module to the shared KMP one.  For example <code class="language-plaintext highlighter-rouge">CharactersDataSource</code> shown below which uses the repository class to fetch data for
a particular page from the GraphQL backend (the repository makes use in turn of the Apollo Kotlin library).</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="code"><pre><span class="kd">class</span> <span class="nc">CharactersDataSource</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">repository</span><span class="p">:</span> <span class="nc">MortyRepository</span><span class="p">)</span> <span class="p">:</span> <span class="nc">PagingSource</span><span class="p">&lt;</span><span class="nc">Int</span><span class="p">,</span> <span class="nc">CharacterDetail</span><span class="p">&gt;()</span> <span class="p">{</span>

    <span class="k">override</span> <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">load</span><span class="p">(</span><span class="n">params</span><span class="p">:</span> <span class="nc">LoadParams</span><span class="p">&lt;</span><span class="nc">Int</span><span class="p">&gt;):</span> <span class="nc">LoadResult</span><span class="p">&lt;</span><span class="nc">Int</span><span class="p">,</span> <span class="nc">CharacterDetail</span><span class="p">&gt;</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">pageNumber</span> <span class="p">=</span> <span class="n">params</span><span class="p">.</span><span class="n">key</span> <span class="o">?:</span> <span class="mi">0</span>

        <span class="kd">val</span> <span class="py">charactersResponse</span> <span class="p">=</span> <span class="n">repository</span><span class="p">.</span><span class="nf">getCharacters</span><span class="p">(</span><span class="n">pageNumber</span><span class="p">)</span>
        <span class="kd">val</span> <span class="py">characters</span> <span class="p">=</span> <span class="n">charactersResponse</span><span class="p">.</span><span class="n">results</span><span class="p">.</span><span class="nf">mapNotNull</span> <span class="p">{</span> <span class="n">it</span><span class="o">?.</span><span class="n">characterDetail</span> <span class="p">}</span>

        <span class="kd">val</span> <span class="py">prevKey</span> <span class="p">=</span> <span class="k">if</span> <span class="p">(</span><span class="n">pageNumber</span> <span class="p">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">pageNumber</span> <span class="p">-</span> <span class="mi">1</span> <span class="k">else</span> <span class="k">null</span>
        <span class="kd">val</span> <span class="py">nextKey</span> <span class="p">=</span> <span class="n">charactersResponse</span><span class="p">.</span><span class="n">info</span><span class="p">.</span><span class="n">next</span>
        <span class="k">return</span> <span class="nc">LoadResult</span><span class="p">.</span><span class="nc">Page</span><span class="p">(</span><span class="n">data</span> <span class="p">=</span> <span class="n">characters</span><span class="p">,</span> <span class="n">prevKey</span> <span class="p">=</span> <span class="n">prevKey</span><span class="p">,</span> <span class="n">nextKey</span> <span class="p">=</span> <span class="n">nextKey</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">getRefreshKey</span><span class="p">(</span><span class="n">state</span><span class="p">:</span> <span class="nc">PagingState</span><span class="p">&lt;</span><span class="nc">Int</span><span class="p">,</span> <span class="nc">CharacterDetail</span><span class="p">&gt;):</span> <span class="nc">Int</span><span class="p">?</span> <span class="p">{</span>
        <span class="k">return</span> <span class="k">null</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>That Android module had also included view models where <code class="language-plaintext highlighter-rouge">Pager</code> instances had been created.
That code, along with those View Models, were also moved to shared code (making use of
the <a href="https://github.com/rickclephas/KMP-ObservableViewModel">KMP-ObservableViewModel</a> library
to share the view models across the platforms)</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="code"><pre><span class="k">open</span> <span class="kd">class</span> <span class="nc">CharactersViewModel</span><span class="p">():</span> <span class="nc">ViewModel</span><span class="p">(),</span> <span class="nc">KoinComponent</span> <span class="p">{</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">repository</span><span class="p">:</span> <span class="nc">MortyRepository</span> <span class="k">by</span> <span class="nf">inject</span><span class="p">()</span>

    <span class="kd">val</span> <span class="py">charactersFlow</span><span class="p">:</span> <span class="nc">Flow</span><span class="p">&lt;</span><span class="nc">PagingData</span><span class="p">&lt;</span><span class="nc">CharacterDetail</span><span class="p">&gt;&gt;</span> <span class="p">=</span> <span class="nc">Pager</span><span class="p">(</span><span class="nc">PagingConfig</span><span class="p">(</span><span class="n">pageSize</span> <span class="p">=</span> <span class="mi">20</span><span class="p">))</span> <span class="p">{</span>
        <span class="nc">CharactersDataSource</span><span class="p">(</span><span class="n">repository</span><span class="p">)</span>
    <span class="p">}.</span><span class="n">flow</span><span class="p">.</span><span class="nf">cachedIn</span><span class="p">(</span><span class="n">viewModelScope</span><span class="p">.</span><span class="n">coroutineScope</span><span class="p">)</span>

    <span class="o">..</span><span class="p">.</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>The open question then was what was needed in the shared code to expose that data to the iOS SwiftUI client and thankfully
received <a href="https://x.com/ianhlake/status/1790609545892671795">following suggestion</a> from Ian Lake!</p>

<blockquote>
  <p>I’d be interested if you’re able to hook up the (newly public) PagingDataPresenter to something like UICollectionViewDiffableDataSource to get Paging hooked up to a SwiftUI based UI. In theory, you should have everything you need to get that working!</p>
</blockquote>

<p>These are the changes added then to the view model to make us of <code class="language-plaintext highlighter-rouge">PagingDataPresenter</code>.  This code hooks up that class to the
<code class="language-plaintext highlighter-rouge">Pager</code> one shown above.  Note use of that <code class="language-plaintext highlighter-rouge">getElement</code> function below.  We need to call this from the SwiftUI client as per
<a href="https://x.com/ianhlake/status/1791213139968581955">following comnent</a> from Ian to replicate functionality in
<a href="https://github.com/androidx/androidx/blob/androidx-main/paging/paging-compose/src/commonMain/kotlin/androidx/paging/compose/LazyPagingItems.kt">LazingPagingItems</a> that also calls <code class="language-plaintext highlighter-rouge">charactersPagingDataPresenter.get()</code>.</p>

<blockquote>
  <p>Yes, that’s how it is supposed to work (and the big difference between peek() and get()) - get() is what sends the (internal) view port hints that is what triggers the automatic loading as you scroll down/up beyond the data already loaded.</p>
</blockquote>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
</pre></td><td class="code"><pre><span class="k">private</span> <span class="kd">val</span> <span class="py">charactersPagingDataPresenter</span> <span class="p">=</span> <span class="kd">object</span> <span class="err">: </span><span class="nc">PagingDataPresenter</span><span class="p">&lt;</span><span class="nc">CharacterDetail</span><span class="p">&gt;()</span> <span class="p">{</span>
    <span class="k">override</span> <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">presentPagingDataEvent</span><span class="p">(</span><span class="n">event</span><span class="p">:</span> <span class="nc">PagingDataEvent</span><span class="p">&lt;</span><span class="nc">CharacterDetail</span><span class="p">&gt;)</span> <span class="p">{</span>
        <span class="nf">updateCharactersSnapshotList</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="nd">@NativeCoroutinesState</span>
<span class="kd">val</span> <span class="py">charactersSnapshotList</span> <span class="p">=</span> <span class="nc">MutableStateFlow</span><span class="p">&lt;</span><span class="nc">ItemSnapshotList</span><span class="p">&lt;</span><span class="nc">CharacterDetail</span><span class="p">&gt;&gt;(</span><span class="n">viewModelScope</span><span class="p">,</span> <span class="n">charactersPagingDataPresenter</span><span class="p">.</span><span class="nf">snapshot</span><span class="p">())</span>

<span class="nf">init</span> <span class="p">{</span>
    <span class="n">viewModelScope</span><span class="p">.</span><span class="n">coroutineScope</span><span class="p">.</span><span class="nf">launch</span> <span class="p">{</span>
        <span class="n">charactersFlow</span><span class="p">.</span><span class="nf">collectLatest</span> <span class="p">{</span>
            <span class="n">charactersPagingDataPresenter</span><span class="p">.</span><span class="nf">collectFrom</span><span class="p">(</span><span class="n">it</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">private</span> <span class="k">fun</span> <span class="nf">updateCharactersSnapshotList</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">charactersSnapshotList</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">charactersPagingDataPresenter</span><span class="p">.</span><span class="nf">snapshot</span><span class="p">()</span>
<span class="p">}</span>

<span class="k">fun</span> <span class="nf">getElement</span><span class="p">(</span><span class="n">index</span><span class="p">:</span> <span class="nc">Int</span><span class="p">):</span> <span class="nc">CharacterDetail</span><span class="p">?</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">charactersPagingDataPresenter</span><span class="p">.</span><span class="k">get</span><span class="p">(</span><span class="n">index</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>
<h3 id="ios-swiftui-client-code">iOS SwiftUI client code</h3>

<p>The following is the SwiftUI code to show the list of characters.  It makes use of <code class="language-plaintext highlighter-rouge">CharactersViewModel</code> from the shared
KMP code and also functionality provided by the <a href="https://github.com/rickclephas/KMP-ObservableViewModel">KMP-ObservableViewModel</a>
library to allow a <code class="language-plaintext highlighter-rouge">MutableStateFlow</code> from the shared code to appear as a standard observable Swift property (in this case for <code class="language-plaintext highlighter-rouge">charactersSnapshotList</code>).
Note the call to <code class="language-plaintext highlighter-rouge">getElement</code> as mentioned above.  I’m not certain there isn’t a better way to do this and will update the
article if I receive any feedback about that.</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="code"><pre><span class="kd">struct</span> <span class="kt">CharactersListView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@StateViewModel</span> <span class="k">var</span> <span class="nv">viewModel</span> <span class="o">=</span> <span class="kt">CharactersViewModel</span><span class="p">()</span>
    
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">List</span> <span class="p">{</span>
            <span class="kt">ForEach</span><span class="p">(</span><span class="n">viewModel</span><span class="o">.</span><span class="n">charactersSnapshotList</span><span class="o">.</span><span class="n">indices</span><span class="p">,</span> <span class="nv">id</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="p">{</span> <span class="n">index</span> <span class="k">in</span>
                <span class="k">if</span> <span class="k">let</span> <span class="nv">character</span> <span class="o">=</span> <span class="n">viewModel</span><span class="o">.</span><span class="nf">getElement</span><span class="p">(</span><span class="nv">index</span><span class="p">:</span> <span class="kt">Int32</span><span class="p">(</span><span class="n">index</span><span class="p">))</span> <span class="p">{</span>
                    <span class="kt">CharactersListRowView</span><span class="p">(</span><span class="nv">character</span><span class="p">:</span> <span class="n">character</span><span class="p">)</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">navigationTitle</span><span class="p">(</span><span class="s">"Characters"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br /></p>
<h3 id="android-compose-client-code">Android Compose client code</h3>

<p>The Android Compose code is more or less the same as it was before.  The only difference is use of that same <code class="language-plaintext highlighter-rouge">CharactersViewModel</code> used by the
iOS SwiftUI client.</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="code"><pre><span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">CharactersListView</span><span class="p">(</span><span class="n">characterSelected</span><span class="p">:</span> <span class="p">(</span><span class="n">character</span><span class="p">:</span> <span class="nc">CharacterDetail</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">viewModel</span><span class="p">:</span> <span class="nc">CharactersViewModel</span> <span class="p">=</span> <span class="nf">koinInject</span><span class="p">()</span>
    <span class="kd">val</span> <span class="py">lazyCharacterList</span> <span class="p">=</span> <span class="n">viewModel</span><span class="p">.</span><span class="n">charactersFlow</span><span class="p">.</span><span class="nf">collectAsLazyPagingItems</span><span class="p">()</span>

    <span class="nc">LazyColumn</span> <span class="p">{</span>
        <span class="nf">items</span><span class="p">(</span>
            <span class="n">count</span> <span class="p">=</span> <span class="n">lazyCharacterList</span><span class="p">.</span><span class="n">itemCount</span><span class="p">,</span>
            <span class="n">key</span> <span class="p">=</span> <span class="n">lazyCharacterList</span><span class="p">.</span><span class="nf">itemKey</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">id</span> <span class="p">}</span>
        <span class="p">)</span> <span class="p">{</span> <span class="n">index</span> <span class="p">-&gt;</span>
            <span class="kd">val</span> <span class="py">character</span> <span class="p">=</span> <span class="n">lazyCharacterList</span><span class="p">[</span><span class="n">index</span><span class="p">]</span>
            <span class="n">character</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span>
                <span class="nc">CharactersListRowView</span><span class="p">(</span><span class="n">character</span><span class="p">,</span> <span class="n">characterSelected</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p><br />
Featured in <a href="https://androidweekly.net/issues/issue-623">Android Weekly #623</a></p>

<p><br /></p>
<h3 id="related-tweet">Related tweet</h3>

<div class="jekyll-twitter-plugin"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Consuming Jetpack Paging KMP code in SwiftUI and Compose clients <a href="https://t.co/iqAu2ukKcN">https://t.co/iqAu2ukKcN</a><br /><br />As mentioned in article, not 100% certain about SwiftUI implementation here but will update if/when I find better way (and thanks again to <a href="https://twitter.com/ianhlake?ref_src=twsrc%5Etfw">@ianhlake</a> for pointers)</p>&mdash; John O&#39;Reilly (@joreilly) <a href="https://twitter.com/joreilly/status/1791524855382208865?ref_src=twsrc%5Etfw">May 17, 2024</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</div>]]></content><author><name></name></author><summary type="html"><![CDATA[This week saw the first Kotlin Multiplatform (KMP) stable release of the Jetpack Paging library (following on from a flurry of Jetpack KMP related announcements recently!). In this article I’m going to outline changes made to add use of that library to the Morty KMP sample and in particular show how that could be consumed in the associated SwiftUI client. We’ll also cover the Android Compose code for completeness but that works as it did before when using the previously Android only version of that library.]]></summary></entry></feed>