VocaFlow is a powerful language learning application built using Kotlin Multiplatform technology. Designed to assist language learners in expanding their vocabulary, VocaFlow provides an immersive and interactive experience for users to learn words and phrases in various languages.
With VocaFlow, language learners can explore a wide range of languages and effortlessly build their vocabulary through engaging exercises and interactive quizzes. The application leverages the Kotlin Multiplatform framework, allowing it to run seamlessly on multiple platforms, including Android, iOS.
Language Diversity: VocaFlow offers an extensive collection of languages to learn from, catering to both popular languages and niche dialects. Users can choose their desired language pairings and easily switch between them to broaden their linguistic horizons.
Vocabulary Expansion: VocaFlow provides users with a comprehensive database of words and phrases specific to each language. Through intuitive exercises and quizzes, learners can practice and reinforce their understanding of vocabulary in context.
![]() |
![]() |
![]() |
![]() |
---|
![]() |
![]() |
![]() |
---|
- Tech-stack
- 100% Kotlin
- Coroutines - perform background operations
- Kotlin Flow - data flow across all app layers, including views
- Kotlin multiplatform - The Kotlin Multiplatform technology is designed to simplify the development of cross-platform projects.
- Compose Multiplatform - Develop stunning shared UIs for Android, iOS, desktop, and web.
- Koin - dependency injection (dependency retrieval)
- Voyager - A multiplatform navigation library built for, and seamlessly integrated with, Jetpack Compose
- Napier - Napier is a logger library for Kotlin Multiplatform
- InsetX - InsetX is a Kotlin Multiplatform library for managing window insets
- 100% Kotlin
- Modern Architecture
- UI
- Compose Multiplatform - Develop stunning shared UIs for Android, iOS, desktop, and web.
- Material Design 3 - application design system providing UI components
- CI ...
- Testing ...
- Static analysis tools (linters)
- Detekt - verify code complexity and code smells
- Gradle
- Gradle Kotlin DSL - define build scripts
- Versions catalog - define dependencies
- GitHub Boots ...
This layer is closest to what the user sees on the screen.
The presentation
layer uses MVI
pattern.
MVI
- action
modifies the common UI state
and emits a new state to a view via MutableState
The
common state
is a single source of truth for each view. This solution derives from Unidirectional Data Flow and Redux principles.
Components:
- View (Composable Function) - observes common view state (through
MutableState
). Pass user interactions toViewModel
. Views are hard to test, so they should be as simple as possible. - ViewModel - emits (through
MutableState
) view state changes to the view and deals with user interactions (these view models are not simply POJO classes). - UIModel - sometimes you may not have enough DomainModel to display UI states. For this purpose, the UIModel will be used, supplementing the domain model with the necessary attributes.
- Mapper - maps
domain model
toui model
(to keeppresentation
layer independent from thedomain
layer). - State - data class that holds the state content of the corresponding screen e.g. list of
User
, loading status etc. The state is exposed as a Compose runtimeMutableState
object from that perfectly matches the use-case of receiving continuous updates with initial value. - Action - plain object that is sent through
ViewModel
to thereducer
. Actions should reflect UI events caused by the user. Actions provide byreducer
and should update state.
For example(ViewModel
):
...
override fun setInitialState(): State = State()
override fun onReduceState(action: Action): State = when (action) {
is Loading -> currentState.copy(
isLoading = true,
isError = false,
)
is GetCurrentUser -> currentState.copy(
isLoading = false,
isError = false,
currentUser = action.currentUser,
)
is Action.Error -> currentState.copy(
isLoading = false,
isError = true,
errorMessage = action.errorMessage,
)
}
...
- Effect - plain object that signals one-time side-effect actions that should impact the UI e.g. triggering a navigation action, showing a Toast, SnackBar etc. Effects are exposed as
ChannelFlow
which behave as in each event is delivered to a single subscriber. An attempt to post an event without subscribers will suspend as soon as the channel buffer becomes full, waiting for a subscriber to appear.
Every screen defines its own contract class that states all corresponding core components described above: state, actions and effects.
For example:
class FirebaseTestContract {
data class State(
val currentUser: FirebaseUser? = null,
val isLoading: Boolean = false,
val isError: Boolean = false,
val errorMessage: String = "",
) : BaseViewState
sealed interface Action : BaseViewAction {
object Loading : Action
data class GetCurrentUser(val currentUser: FirebaseUser?) : Action
data class Error(val errorMessage: String) : Action
}
sealed interface Effect : BaseViewEffect {
object NavigationBack : Effect
}
}
- UseCase - contains business logic
- DomainModel - defines the core structure of the data that will be used within the application. This is the source of truth for application data.
- Repository interface - required to keep the
domain
layer independent from thedata layer
(Dependency inversion).
Encapsulates application data. Provides the data to the domain
layer eg. retrieves data from the internet and cache the data in disk cache (when the device is offline).
Components:
- Repository is exposing data to the
domain
layer. Depending on the application structure and quality of the external API repository can also merge, filter, and transform the data. These operations intend to create a high-quality data source for thedomain
layer. It is the responsibility of the Repository (one or more) to construct Domain models by reading from theData Source
and accepting Domain models to be written to theData Source
- Mapper - maps
data model
todomain model
(to keepdomain
layer independent from thedata
layer). - Data Sources - This application has two
Data Sources
-remote
(used for network access) andcache
(local storage used to access device persistent memory). These data sources can be treated as an implicit sub-layer.
Gradle versions catalog is used as a centralized dependency management third-party dependency coordinates (group, artifact, version) are shared across all modules (Gradle projects and subprojects).
All of the dependencies are stored in the settings.gradle.kts file (default location). Gradle versions catalog consists of a few major sections:
[versions]
- declare versions that can be referenced by all dependencies[libraries]
- declare the aliases to library coordinates[bundles]
- declare dependency bundles (groups)[plugins]
- declare Gradle plugin dependencies
π¬π§ πΊπ¦
The approach for using localization in KMM takes from Medium
Example:
Text(
text = getString(id = "name"),
)
Text(
text = getString(id = "number", quantity = 1),
)
Text(
text = getString(id = "text_by_args", args = arrayOf("1", "2", "3")),
)