<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Thinksoft on Medium]]></title>
        <description><![CDATA[Stories by Thinksoft on Medium]]></description>
        <link>https://medium.com/@thinksoft?source=rss-cd827584d069------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/2*Ki_ZDRR0q7uzHPULYsf6hw.jpeg</url>
            <title>Stories by Thinksoft on Medium</title>
            <link>https://medium.com/@thinksoft?source=rss-cd827584d069------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 10 Nov 2026 05:48:45 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@thinksoft/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Kotlin Asynchronous Flow for UI validations]]></title>
            <link>https://medium.com/@thinksoft/kotlin-asynchronous-flow-for-ui-validations-4b5957d55205?source=rss-cd827584d069------2</link>
            <guid isPermaLink="false">https://medium.com/p/4b5957d55205</guid>
            <category><![CDATA[reactor]]></category>
            <category><![CDATA[rxjava]]></category>
            <category><![CDATA[validation]]></category>
            <category><![CDATA[reactive]]></category>
            <category><![CDATA[kotlin]]></category>
            <dc:creator><![CDATA[Thinksoft]]></dc:creator>
            <pubDate>Fri, 10 Apr 2020 16:47:03 GMT</pubDate>
            <atom:updated>2020-04-10T16:47:03.485Z</atom:updated>
            <content:encoded><![CDATA[<h3>Conditions:</h3><ol><li>Username EditText</li><li>Email EditText</li><li>Password EditText</li><li>Confirm Password EditText</li><li>“Proceed” Button</li></ol><h3>Task:</h3><p>Implement effective interactive validation for input data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/380/1*F0BaaElxUfpW0ApPcJ5MBQ.gif" /></figure><h3>Solution:</h3><p>Serial input of text data by a user can be considered as a data stream and processed with the help of related instruments — Asynchronous Stream SDKs.</p><p>There are some benefits of processing data as Asynchronous Stream:</p><ul><li>Absence of callback hell</li><li>Ability to combine last emitted items without adding last_Item variable</li><li>Most Asynchronous Stream SDKs have rich API for dealing with a wide range of tasks of combining data streams.</li></ul><p>The task can be implemented with the help of the next Asynchronous Stream SDKs:</p><ul><li>RxJava</li><li>Reactor</li><li>Reactive Streams</li><li>etc.</li></ul><p>In this article, the <strong>Kotlinx.coroutines.flow </strong>SDK will be considered as an instrument to accomplish the task.</p><p>In most cases, an Android component that receives text data entered by a user is <strong>EditText</strong>. The changes in text data input can be tracked via <strong>EditText.addTextChangedListener</strong> callback. Since the <strong>EditText.addTextChangedListener</strong> callback doesn’t have a built-in ability to be converted to <strong>Kotlinx.coroutines.flow </strong>stream there is a need to implement such conversion.</p><p>To convert the <strong>EditText.addTextChangedListener</strong> callback into the asynchronous stream, <strong>Flow</strong> and <strong>Channel</strong> extensions of <strong>kotlinx.coroutines</strong> library can be used. Here is an example:</p><pre><strong>fun </strong>EditText.asFlow(resetTarget : () -&gt; Unit): Flow&lt;EditText&gt; {</pre><pre><strong>return </strong><em>Channel</em>&lt;EditText&gt;(capacity = Channel.<strong>UNLIMITED</strong>).<em>also </em><strong>{ </strong>channel <strong>-&gt; </strong>addTextChangedListener(<strong>object </strong>: TextWatcher {</pre><pre><strong>override fun </strong>beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {</pre><pre>}</pre><pre><strong>override fun </strong>onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {</pre><pre>s?.<em>let </em><strong>{</strong></pre><pre><strong>this</strong>@asFlow.<em>clearError</em>()</pre><pre>resetTarget();</pre><pre>channel.offer(<strong>this</strong>@asFlow)<br> <strong>}<br></strong>}</pre><pre><strong>override fun </strong>afterTextChanged(s: Editable?) {</pre><pre>}</pre><pre>})</pre><pre><strong>}</strong>.<em>receiveAsFlow</em>()</pre><pre>}</pre><p>The example above shows how<strong> Channel.offer</strong> is used as a bridge between <strong>EditText.addTextChangedListener</strong> callback and <strong>Flow</strong> since the last one uses suspend methods and requires CoroutineScoupe. <strong>Channel.offer </strong>buffers values that come from <strong>EditText.addTextChangedListener </strong>and converts them to <strong>Flow</strong> with the help of <strong>Channel.receiveAsFlow.</strong></p><p>After the above preparation, the input of an <strong>EditText </strong>can be converted to <strong>Flow,</strong> and all the inputs of <strong>EditText</strong> fields from the condition section can be received as:</p><pre><strong>val </strong>resetProceedButton = {proceedButton.<em>isEnabled </em>= <strong>false}</strong></pre><pre><strong>val </strong>usernameFlow = userNameEditText.<em>asFlow</em>(resetProceedButton)</pre><pre>.<em>filter</em>(EditText::isValidSize)</pre><pre>.<em>debounce</em>(300)</pre><pre>.<em>filter</em>(EditText::isUniqueUsername)</pre><pre><strong>val </strong>emailFlow = emailEditText.<em>asFlow</em>(resetButton)</pre><pre>.<em>filter</em>(EditText::isValidEmail)</pre><pre><strong>val </strong>password1Flow = password1EditText.<em>asFlow</em>(resetButton)</pre><pre>.<em>filter</em>(EditText::isValidSize)</pre><pre><strong>val </strong>password2Flow = password2EditText.<em>asFlow</em>(resetButton)</pre><pre>.<em>filter</em>(EditText::isValidSize)</pre><p><strong>.filter </strong>is used to prevent the user input from going to the next validation steps if the current is not passed.</p><p>Example of the function that is passed to <strong>.filter:</strong></p><pre><strong>suspend fun </strong>EditText.isValidSize():Boolean {</pre><pre><strong>if </strong>(<em>text</em>.<strong>length </strong>&lt; 5) {</pre><pre><em>error </em>= <strong>&quot;Invalid Size&quot;</strong></pre><pre><strong>return false</strong></pre><pre>}</pre><pre><strong>return true</strong></pre><pre>}</pre><p>And</p><pre><strong>suspend fun </strong>EditText.isUniqueUsername():Boolean  {</pre><pre><em>delay</em>(1000)</pre><pre><strong>if </strong>(<em>text</em>.toString() == <strong>&quot;111111&quot;</strong>) { // Here should be call to backend API with proper check of uniqueness of</pre><pre><em>error </em>= <strong>&quot;Invalid username&quot;  </strong>// a username</pre><pre><strong>return false</strong></pre><pre>}</pre><pre><strong>return true</strong></pre><pre>}</pre><p>A good example of how Asynchronous Streams can help to track the latest emitted item is operator <strong>.combine.</strong></p><pre><strong>val </strong>passwordsAreEqualFlow = password1Flow.<em>combine</em>(password2Flow, <strong>this</strong>::comparePasswords)</pre><pre><strong>private suspend fun </strong>comparePasswords(edit1: EditText, edit2: EditText) : Boolean {</pre><pre><strong>if </strong>(edit2.<em>text</em>.toString() == edit1.<em>text</em>.toString()) {</pre><pre>edit1.<em>error </em>= <strong>null</strong></pre><pre>edit2.<em>error </em>= <strong>null</strong></pre><pre><strong>return true</strong></pre><pre>}</pre><pre>edit1.<em>error </em>= <strong>&quot;Passwords are not equal&quot;</strong></pre><pre>edit2.<em>error </em>= <strong>&quot;Passwords are not equal&quot;</strong></pre><pre><strong>return false</strong></pre><pre>}</pre><p>Whenever a user types in the edit1 EditText, <strong>comparePasswords </strong>will receive the edit2 EditText with its latest state.</p><p>Since <strong>Flow</strong> is cold observable, it requires the terminal operator to start the processing of items. The<strong> .collect </strong>operator<strong> </strong>is one of such terminal operators.</p><pre><strong>coroutineContextOnStart</strong>.<em>launch </em><strong>{</strong></pre><pre><em>combine</em>(usernameFlow, emailFlow, passwordsAreEqualFlow, ::enableProceedButton)</pre><pre>.<em>onEach </em><strong>{ </strong>proceedButton.<em>isEnabled </em>= <strong>it }</strong></pre><pre>.<em>collect</em>()</pre><pre><strong>}</strong></pre><h3>Issues:</h3><p>In case of replacing <strong>.filter </strong>with more common <strong>.onEach </strong>+ throw Exception + <strong>.retryWhen </strong>the operator<strong><em> </em>.combine </strong>forgets last value of all flows except the current active. It happens because of the recreation of the flows after an exception is thrown at any point of the stream despite the presence of <strong>.catch</strong> operator.</p><h3>Conclusion:</h3><p><strong>Kotlinx.coroutines.flow</strong> provides an excellent and perspective API for dealing with asynchronous streams of data.</p><p>But as the @ExperimentalCoroutinesApi annotation says it is mostly experimental. It seems that RxJava (RxKotlin) is still an excellent way to handle asynchronously emitted items.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4b5957d55205" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>