We launched our literacy app, Duolingo ABC, on the iOS platform in 2020. With any new product, we at Duolingo tend to focus on a single platform—making tweaks in isolation, iterating quickly, and pivoting to new ideas—before bringing the experience to other platforms. Duolingo ABC exceeded our expectations shortly after its iOS debut, winning a spot on Time’s Best Inventions of 2020 and a 2021 Webby award. Knowing we had hit the mark, it was time to bring the experience to Android!

Why Android?

For our flagship Duolingo app, Android users make up more than half of our user base—that’s more than iOS and Web combined!

However, Android can be a fickle platform to develop for, especially for kids devices. Android devices come in a variety of screen sizes, performance capabilities, even custom operating systems (looking at you, Amazon)! To ensure a universally smooth experience, Android code architecture is of the utmost importance. We decided to build the Duolingo ABC Android app from the ground up, using learnings from our recent Android Reboot initiative. This approach granted us the opportunity to embrace new tooling and architecture patterns to create a delightful, consistent experience for kids!

Animations: a love-hate relationship

Duolingo ABC aims to help young children, aged 3 to 8, learn and love to read. Designing an engaging and enriching experience for kids requires balancing a visually joyful experience with explicit literacy instruction. As a result, Duolingo ABC relies heavily on audio narration and animation.

Animations are a useful educational tool, and can help reinforce a learning concept by visualizing its components. A great example is how we teach the fundamental skill of decoding, or sounding out words. Decoding involves pronouncing individual letters or letter teams and then blending the sounds together. Encoding is the reverse process, used for spelling words. Duolingo ABC uses animations to guide kids step-by-step through these processes:

Decoding

Encoding

Animations also provide intrinsic motivation to keep young learners excited about their lessons. Getting a visit from our mascot Duo during an exercise, watching letters snap into place in your name, or a generous dose of sparkles from a correct answer all help add delight and satisfaction to the learning journey! Ensuring crisp, consistent animations is key to our app’s success.

As an app developer, I dread animations. Animations are an example of an asynchronous task, which just means that they run independently of the rest of the app. One-off animations are straightforward, but complexity ramps up as we start grouping animations or combining them with other tasks, such as playing audio. Let’s take a simple example of playing multiple audio files sequentially alongside an animation:

Here we introduce how certain letters together produce a specific sound – The letter team u, e says ‘/u/’. Our curriculum covers multiple letter teams, such as “a_e” in “cake” or “ie” in “pie”. To keep this exercise generalized, the sentence narration above is actually composed of multiple narrations files:

The letter team + [first letter] + [second letter] + says + [phoneme].

Playing the sentence audio requires a sequential execution of asynchronous tasks. To add another layer of complexity, certain narrations are accompanied by visual cues, such as the icon pulsing with the audio “flute”. This is an example of a simultaneous execution of asynchronous tasks.

Coordinating asynchronous tasks—such as animations or audio—often relies on the callback pattern, where you can designate a block of code to be executed upon a task’s completion. This solves the sequential case of audio narration above, because we connect a block of code to the first async task, which then sets off another block of code to start the next async task, and so on (it’s a domino effect).

However, this style is difficult to maintain since we have multiple code blocks floating around. Callbacks also lack a clear solution to the simultaneous case of playing animations and audio together. How can we guarantee that the word pulse and accompanying “/u/” audio cue both play prior to the icon pulse?

Kotlin Coroutines, an asynchronous framework, offers a solution to this problem. Kotlin Coroutines simplify how we write and organize asynchronous code, allowing us to write it as we would regular, synchronous code. We still use the callback pattern, but we can wrap it in a more useful representation: a suspending function. Suspending functions do the work of setting up and waiting for a callback but hide the details of doing so. Under the hood, Kotlin will handle all the work of creating the appropriate callbacks, awaiting their execution, and then proceeding with our code. The result is code that we would see in the synchronous world:

Callbacks

Coroutines

playAudio(“the_letter_team.mp3”) {

  playAudio(“letter_u.mp3”) {

    playAudio(“letter_e.mp3”) {

      playAudio(“says.mp3”) {

        playAudio(“phoneme_u.mp3”)

        …

      }

    }

  }

}

playAudio(“the_letter_team.mp3”)

playAudio(“letter_u.mp3”)

playAudio(“letter_e.mp3”)

playAudio(“says.mp3”)

playAudio(“phoneme_u.mp3”)

Suspending methods execute sequentially—each method must complete before the next starts. We may want to run suspending functions together in certain cases, such as pulsing an icon while narrating a word. To handle this, Kotlin Coroutines offer two useful functionalities: async and await. `Async` allows us to kick off suspending code in one place and await its completion in another. We can pair this with another method, `awaitAll`, to run multiple tasks simultaneously but wait for all to complete before continuing on:





awaitAll(

  async { wordView.animatePulse() },

  async { playAudio(phoneme) },

)


awaitAll(

  async { iconView.animatePulse() },

  async { playAudio(word) },

)


continueButton.animateShow()

Kotlin Coroutines simplify the headache of coordinating the many moving parts of Duolingo ABC, adding certainty and consistency to our app!

The need for speed (and modules)

High-quality, accessible literacy education is vital to our company mission, and the ability to reach more learners via Android was a key milestone. In order to speed up development, we invested in speeding up our developers.

When writing code—either making improvements or adding new features— developers need to rebuild the application to test each change. Waiting for a new build may seem like part of the job, but build times can add up as a project becomes more complex. Creating a fresh build of our Duolingo ABC app takes 1 to 2 minutes, which can stack up over the course of a day. The more time we’re waiting for builds, the less time we have to write code!

Modularization is the process of separating and organizing code into distinct pieces. Consider the different features within the Duolingo ABC app:

Onboarding

Home

Lesson

Think about it: The onboarding flow has nothing to do with the lesson flow. Verifying an email address doesn’t require the grading logic for a spelling exercise, and vice versa. We consider these flows to be disjoint from one another, so we organize onboarding and lesson code into their own areas, or “modules,” in the codebase. Using modules encourages a mindset of dividing a codebase into small pieces, each with a specific purpose. The result is that each feature doesn't rely on—or even know about—other features.

Often, two features will share a similar concept. Lesson and home features both require knowledge of the curriculum, or course, that we teach. Lessons require this information to generate learning exercises while the home screen visualizes course content as different, themed levels. In these cases, we organize the shared code into a library module, which features can access independently. This is known as a multi-module pattern, which helps limit what code is visible to and/or used by any given module:

Modularization helps speed up our build times, and consequently our development. By default, building the application requires building each module sequentially. We have to wait for each module before moving on to the next.

However, enforcing strict boundaries between modules allows us to use parallelization, a build process that can build disjoint modules at the same time.

During development, we may not need to include all features in each build. We can leverage modularization to create partial builds that exclude unnecessary features. Let’s say we’re developing a new lesson flow for older kids that includes a reading experience for longer Stories. Given our module organization, the new reading lesson code is unlikely to impact other features, such as onboarding or home. We can leverage this notion of testing in isolation by building only the reading lesson feature. This way we save time by skipping the build steps for any unnecessary modules!

These build options allow us to iterate quickly on new features, getting them into the hands of learners sooner!

Learners, and readers, first

Building an app from scratch means architecting an experience around our learners. We took the opportunity to leverage new engineering patterns to create high-quality features and deliver them faster. We stand by our mission to help kids to learn and love to read, and that is why we built Duolingo ABC on Android with kids in mind! If you have a little learner in your life, be sure to check us out Android or iOS!