This project follows the MVVM (Model-View-ViewModel) architecture pattern. The diagram below shows how different components interact with each other as a unit.
Click to expand
The View Layer (managed by MainActivity
or Fragment
) is responsible for displaying UI
components, handling fragment navigation, and interacting with the WebView
to render HTML. User
interactions are propagated to the ViewModel
, which processes and stores the results as a State
in LiveData
. The Fragment
observes the LiveData
to update the UI based on changes in the
State
.
Each Fragment
uses FragmentResultKey
,
an interface primarily implemented by enums, to ensure unique keys for communication. It provides a
key()
method, which is used in various ways to manage data exchange between fragments:
- In
Bundle.putString()
,Bundle.putInt()
, and similar methods to pass fragment results or arguments between fragments. - In
Fragment.parentFragmentManager.setFragmentResult()
to return a fragment result along with aBundle
. - In
Fragment.parentFragmentManager.setFragmentResultListener()
to listen for results from other fragments. - In
SavedStateHandle.get()
to retrieve an argument from a previous fragment.
That way, whenever we want to exchange data between fragments, we can simply check their
FragmentResultKey
implementations, without worrying about key clashes.
D3.js is used as the primary library for rendering charts. The
JavaScript files, located in assets/chart/
, handle the chart logic
and visualization.
To enable interaction between Kotlin and JavaScript, we implement a binding layer in
assetbinding/chart/
. Each
binding maps a JavaScript function to its Kotlin counterpart. JavaScript interacts with Kotlin via
the JsInterface
class,
registered using WebView.addJavascriptInterface()
for secure communication.
The ViewModel Layer acts as a bridge between the View Layer and the Model Layer. It
handles presentation logic and prepares data to be displayed in the UI. ViewModel
are
lifecycle-aware, which means they can persist through configuration changes, such as when the device
is rotated, and continue to manage UI-related data without being recreated.
To effectively manage data observed by the UI, this project uses:
SafeLiveData
: A null-safe wrapper aroundLiveData
for handling persistent and reactive UI data.UiEvent
: A generic state wrapper for one-time operation like displaying a snackbar, toast, etc.
Both SafeLiveData
and UiEvent
are used to represent the UI's State
, reflecting its current
status — such as text, colors, or other UI properties.
The ModelChangedListener
is registered with the Repository
to listen for changes in the LocalDatabase
. Whenever data
changes, the Repository
notifies all registered listeners.
The ModelSyncListener
is usually used alongside ModelSynchronizer
or InfoSynchronizer
to
handle data updates automatically. This eliminates the need to manually override
ModelChangedListener
methods for each implementation.
The Model Layer serves as the foundation for managing business logic, data processing, and data persistence. It handles interactions between the application and the underlying data sources, such as local databases, remote APIs, or in-memory data structures.
Model
: The base interface for all data stored in theLocalDatabase
, excluding Full-Text Search (FTS) related models. It represents the complete structure of an entity and is used for operations requiring full data access.Info
: A lightweight version of aModel
that includes only the necessary fields. It's designed to optimize SQLSELECT
queries by reducing overhead when full model details aren't needed.GithubReleaseModel
: A class that contains information about app updates retrieved from GitHub.- Data Stored in
SharedPreferences
: User configurations, such as the user's language preference, are stored inSharedPreferences
. The data is accessed using specific keys, which are typically defined as constants, like those in theSettingsPreferences
.