This document describes the Model-View-Controller (MVC) architecture of the LimeIME user interface components. The architecture improves code organization, testability, and maintainability through clear separation of concerns.
Architecture Status: ✅ IMPLEMENTED
Last Updated: December 26, 2025
Version: 1.1 - Major refactoring: Moved handlers/managers to ui/ level, enhanced threading safety, moved NavigationDrawerCallbacks to NavigationManager
The UI architecture is designed to achieve the following objectives:
- Clean Separation of Concerns - Distinct layers for View, Controller/Handler, and Model
- Testability - Controllers and managers can be unit tested without Android context
- Maintainability - Clear responsibilities make code easier to understand and modify
- Reusability - Shared controllers and managers across multiple views
- Code Organization - Consistent patterns and structure throughout the codebase
- Data Integrity - Centralized data access through controllers and managers
The View layer consists of all user-facing components that display data and receive user input.
Components:
- Activities:
MainActivity- Main coordinator and dependency container - Fragments:
NavigationDrawerFragment- Navigation menuSetupImFragment- IM setup and configurationManageImFragment- IM record managementManageRelatedFragment- Related phrase management
- Dialogs:
ImportDialog- Import IM tableShareDialog- Share IM and related tableManageImAddDialog- Add/edit IM recordsSetupImLoadDialog- Load IM filesManageImKeyboardDialog- Keyboard management
Preference Screen:
- Activity:
LIMEPreference(hosts the preferences UI) - Fragment:
LIMEPreference.PrefsFragment(extendsPreferenceFragmentCompat) - Resources: Loads from
res/xml/preference.xml(andres/xml-v17/preference.xml)
Preference Responsibilities:
- Inflate preferences from XML and display settings
- Register/unregister
OnSharedPreferenceChangeListener - React to specific keys (e.g.,
phonetic_keyboard_type) and update runtime behavior viaSearchServer - Persist and restore settings through shared preferences
Responsibilities:
- Display data to users
- Receive and process user input
- Delegate business logic to controllers/managers
- Implement view interfaces for callback notifications
- Handle UI-specific operations (animations, transitions)
Key Constraint: Views do NOT directly access Model layer (SearchServer, DBServer) for operations managed by controllers (backup, restore, share, progress dialogs)
This layer mediates between View and Model, handling business logic and specialized operations.
BaseController (Abstract Base Class)
-
Location:
net.toload.main.hd.ui.controller.BaseController -
Thread-Safe UI Operation Methods (NEW):
handleError(view, message, exception)- Posts error handling to main threadshowProgress(view, message)- Posts progress overlay display to main threadhideProgress(view)- Posts progress overlay hide to main threadupdateProgress(view, percentage, status)- Posts progress updates to main threadshowToast(view, message, length)- Posts toast display to main thread
-
Key Feature: All UI operations use
mainHandler.post()to ensure main thread execution- Safe to call from any thread (background executors, callbacks, etc.)
- Prevents "Can't toast on a thread that has not called Looper.prepare()" crashes
-
Foundation for all controllers
-
Error handling patterns
-
View interface management
-
Activity context management
SetupImController
-
Location:
net.toload.main.hd.ui.controller.SetupImController -
New Feature: Implements
ImportDialog.OnImportIMSelectedListener- Added method:
onImportDialogImSelected(String tableName, boolean restoreUserRecords)- handles import dialog table selection - Added method:
setFileToImport(File file)- stores file for import - Moved from MainActivity to controller for better separation of concerns
- Added method:
-
Navigation Operations:
- Fragment switching (ManageIm, Setup, etc.)
- Navigation drawer management
- ActionBar title updates
- Button state calculation and menu loading
-
Import Operations:
- External file imports: text files (.lime/.cin) via
importTxtTable(File, tableName, boolean restoreUserRecords) - External zipped database imports:
.limedbfiles viaimportZippedDb(File, tableName, boolean restoreUserRecords) - Local file imports from SetupImLoadDialog
- Remote download and import via
downloadAndImportZippedDb(tableName, url, boolean restoreUserRecords)with progress tracking
- External file imports: text files (.lime/.cin) via
-
Backup/Restore Operations:
- Complete backup workflow: user records backup + database backup via
performBackup(Uri) - Complete restore workflow: database restore + menu refresh via
performRestore(Uri) - Recovery operations
- Conditional backup-before-import for external imports when user opts to restore
- Complete backup workflow: user records backup + database backup via
-
Export Operations:
- Export IM tables as zipped database via
exportZippedDb(String, Uri) - Export related table as zipped database via
exportZippedDbRelated(Uri)
- Export IM tables as zipped database via
-
Dependencies:
DBServer,SearchServer -
Threading: Uses
ExecutorServicefor download operations; all UI updates via BaseController thread-safe methods
ManageImController
-
Location:
net.toload.main.hd.ui.controller.ManageImController -
Threading Fix (NEW):
- Background executor thread now posts UI updates to main thread via
mainHandler.post() - Prevents UI thread violations when displaying records from background search operations
- Background executor thread now posts UI updates to main thread via
-
Asynchronous Record Management:
- Load records asynchronously via
loadRecordsAsync(String table, String query, boolean searchByCode, int offset, int limit) - Create, read, update, delete IM records synchronously
- Search and filter records with progress callbacks
- Count operations with table validation
- Load records asynchronously via
-
Related Phrase Management:
- Load related phrases via
loadRelatedPhrases(String pWord, int offset, int limit) - Create, read, update, delete related phrases
- Search within phrases
- Phrase associations
- Load related phrases via
-
Keyboard Operations:
- Keyboard list retrieval via
getKeyboardList() - Keyboard selection management via
setIMKeyboard(String table, Keyboard keyboard) - Layout switching
- Keyboard list retrieval via
-
Data Refresh & Progress:
- Record list refreshes with progress callbacks
- Related phrase list refreshes with progress callbacks
- Progress notifications through view interfaces
-
Dependencies:
SearchServer -
Threading: Uses reusable
ExecutorServicefield for asynchronous record loading; all UI updates viamainHandler.post() -
View Interfaces: Implements callbacks for progress tracking and UI updates
IntentHandler (Moved to ui/ level)
-
Location:
net.toload.main.hd.ui.IntentHandler -
External File Imports:
- Validates file types (txt, db, zip)
- Determines target table
- Imports file content
- Triggers view refresh after import
-
Intent Processing:
- ACTION_SEND intents (file sharing)
- ACTION_VIEW intents (file opening)
- Data validation before import
-
Dependencies:
SearchServer,DBServer -
Threading: Safe for main thread calls; delegates heavy I/O to DBServer
ProgressManager (Moved to ui/ level)
-
Location:
net.toload.main.hd.ui.ProgressManager -
Dialog Lifecycle:
- Show progress overlay/dialog with message
- Update progress message
- Update progress percentage
- Dismiss dialog
-
Operation Feedback:
- Long-running operation notifications
- User feedback during processing
- Cancellation handling
-
Dependencies:
Activity, Android views/dialogs -
Threading: All operations wrapped in
runOnUiThread()
ShareManager (Moved to ui/ level)
-
Location:
net.toload.main.hd.ui.ShareManager -
Export Operations:
- Export IM records as text file
- Export IM records as database file
- Export related phrases as text file
- Export related phrases as database file
-
Share Intent Operations:
- Create share intent
- Launch share dialog
- Handle share targets
-
Operation Feedback:
- Uses ProgressManager for long operations
- Success/failure notifications
- File URI generation
-
Dependencies:
MainActivity,SetupImController,ProgressManager,DBServer,SearchServer -
Threading: Safe for main thread; heavy operations delegated to controllers
NavigationManager (Moved to ui/ level, now implements NavigationDrawerCallbacks)
-
Location:
net.toload.main.hd.ui.NavigationManager -
New Feature: Implements
NavigationDrawerFragment.NavigationDrawerCallbacks- Added method:
onNavigationDrawerItemSelected(int position)- handles drawer item selection - Centralizes all navigation logic away from MainActivity
- Added method:
-
Fragment Navigation:
- Create appropriate fragments based on selection
- Manage fragment transactions
- Handle back navigation
-
Navigation State:
- Track current fragment
- Manage navigation history
- ActionBar updates
-
Dependencies:
Activity,SetupImController,MainActivity -
Threading: Safe for main thread; all fragment operations on main thread
ViewUpdateListener (Base Interface)
public interface ViewUpdateListener {
void onError(String message);
void onProgress(int percentage, String status);
}MainActivityView
public interface MainActivityView extends ViewUpdateListener {
void showProgressDialog(String message);
void hideProgressDialog();
void navigateToFragment(int position);
}SetupImView
public interface SetupImView extends ViewUpdateListener {
void updateButtonStates(Map<String, Boolean> states);
void refreshImList();
}ManageImView
public interface ManageImView extends ViewUpdateListener {
void displayRecords(List<Record> records);
void updateRecordCount(int count);
void refreshRecordList();
}ManageRelatedView
public interface ManageRelatedView extends ViewUpdateListener {
void displayRelatedPhrases(List<Related> phrases);
void refreshPhraseList();
}NavigationDrawerView
public interface NavigationDrawerView extends ViewUpdateListener {
void updateMenuItems(List<NavigationMenuItem> items);
void setSelectedItem(int position);
}The Model layer provides data access and business operations without UI concerns.
Components:
-
SearchServer - Database query operations
- Search and filter records
- Record count operations
- Record retrieval by ID, type, or range
- Keyboard operations
- Related phrase queries
-
DBServer - File-level database operations
- Database import from files
- Database export to files
- Database backup and restore
- Table-level operations
- Backup of user records
-
LimeDB - SQL abstraction layer
- SQL query execution
- Table schema management
- Record serialization/deserialization
- Transaction management
-
LIMEPreferenceManager - Application preferences
- User settings persistence
- Preference queries
- Default value management
Characteristics:
- No Android framework dependencies (except Context for file operations)
- No direct view component references
- Exception handling at this layer (not re-thrown to views)
- Null-safe return values (empty collections instead of null)
┌─────────────────────────────────────────────────────────────────┐
│ VIEW LAYER (Refactored) │
│ │
│ ┌─────────────────────┐ │
│ │ MainActivity │ │
│ │ (Coordinator) │ │
│ │ │ │
│ │ Provides Getters: │ │
│ │ • getSetupImCtrl() │ │
│ │ • getManageImCtrl()│ │
│ │ • getShareManager()│ │
│ │ • getProgressMgr() │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌─────────────────────┼─────────────────────┐ │
│ │ │ │ │
│ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │
│ │ Fragments │ │ Dialogs │ │ Handlers │ │
│ │ - SetupIm │ │ - Import │ │ - Intent │ │
│ │ - ManageIm │ │ - Share │ │ - Progress │ │
│ │ - ManageRel │ │ - Add/Edit │ │ - Navigate │ │
│ │ - Navigation│ │ │ │ │ │
│ └─────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └─────────────────────┼─────────────────────┘ │
│ │ │
└───────────────────────────────┼──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ CONTROLLER/HANDLER/MANAGER LAYER ✅ │
│ │
│ ┌────────────────────┐ ┌────────────────────┐ │
│ │ SetupImController │ │ ManageImController │ │
│ │ • Navigation │ │ • Record CRUD │ │
│ │ • Fragments │ │ • Related CRUD │ │
│ │ • Import/Export │ │ • Search/Filter │ │
│ │ • Backup/Restore │ │ • Menu Refresh │ │
│ │ • Button States │ │ • Keyboard Ops │ │
│ └────────┬───────────┘ └────────┬────────────┘ │
│ │ │ │
│ ┌────────┴───────────────────────┴──────────────┐ │
│ │ Specialized Handlers/Managers │ │
│ │ ┌──────────────┐ ┌───────────────────────┐ │ │
│ │ │IntentHandler │ │ ProgressDialogManager │ │ │
│ │ │ShareManager │ │ NavigationManager │ │ │
│ │ └──────────────┘ └───────────────────────┘ │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
└────────────────────────────┼─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ MODEL LAYER (Unchanged) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ SearchServer │ │ DBServer │ │ LIMEPrefMgr │ │
│ │ │ │ │ │ │ │
│ │ - DB Ops │ │ - File Ops │ │ - Preferences │ │
│ │ - Search │ │ - Import/Exp │ │ │ │
│ │ - CRUD │ │ - Backup │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └───────────────┘ │
│ │ │ │
│ └──────────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ LimeDB │ │
│ │ │ │
│ │ - SQL │ │
│ │ - Schema │ │
│ └──────────┘ │
└─────────────────────────────────────────────────────────────────┘
✅ Data Flow:
View → Controller/Handler/Manager → Model → Database
View ← Controller/Handler/Manager ← Model ← Database
Views delegate operations to controllers/managers through clearly defined interfaces:
// Fragment delegates to controller via MainActivity getter
SetupImController controller = mainActivity.getSetupImController();
controller.performBackup(uri);
// Dialog delegates to manager via MainActivity getter
ShareManager shareManager = mainActivity.getShareManager();
shareManager.exportAndShareImTable(selectedTable);
// Manager delegates to ProgressDialogManager
ProgressDialogManager progressMgr = mainActivity.getProgressDialogManager();
progressMgr.show("Exporting...");Controllers and managers are created once at the Activity level and reused:
// MainActivity onCreate()
private SetupImController setupImController;
private ManageImController manageImController;
public void onCreate(Bundle savedInstanceState) {
// ... setup ...
manageImController = new ManageImController(searchServer);
setupImController = new SetupImController(dbServer, searchServer);
}
// Getter provides access to all components
public SetupImController getSetupImController() {
return setupImController;
}Controllers and managers create fragments and dialogs as needed:
// NavigationManager creates fragments based on type
public Fragment createFragment(int position) {
switch (position) {
case NAV_SETUP: return new SetupImFragment();
case NAV_MANAGE: return new ManageImFragment();
// ...
}
}
// ShareManager creates export files
private File createExportFile(String type) {
String filename = type + "_export_" + System.currentTimeMillis();
return new File(cacheDir, filename);
}Views implement interfaces for controller callbacks:
// Fragment implements SetupImView
public class SetupImFragment extends Fragment implements SetupImView {
@Override
public void updateButtonStates(Map<String, Boolean> states) {
// Update UI with new button states
}
@Override
public void onError(String message) {
// Display error to user
}
}
// Controller notifies view of changes
setupView.updateButtonStates(states);
setupView.refreshImList();
setupView.onError("Operation failed");There are two distinct import flows depending on how the user initiates the operation:
User shares file from another app or opens file with LimeIME. Two scenarios based on file type:
User receives or opens .lime or .cin text file:
External App (shares .lime/.cin file)
↓
IntentHandler.processIntent() (validates intent/file)
├─ Extract URI and file info
├─ Validate file type and scheme
├─ Convert InputStream to File
└─ handleImportTxt()
↓
ImportDialog.newInstanceForFile() (shows dialog)
├─ Display available IM types
├─ Only empty IM tables are enabled (non-empty disabled)
└─ User selects target IM table
↓
AlertDialog: "Restore learning data?" (same strings as 1b)
├─ Cancel
├─ Restore (restoreUserRecords = true)
└─ Don't Restore (restoreUserRecords = false)
↓
SetupImController.onImportDialogImSelected(tableName, restoreUserRecords) (callback)
↓
SetupImFragment.importTxtTable(restoreUserRecords)
↓
SetupImController.importTxtTable(file, tableName, restoreUserRecords)
├─ SearchServer.isValidTableName() (validation)
├─ SearchServer.backupUserRecords() (if restore enabled)
├─ DBServer.importTxtTable() (file import)
├─ SearchServer.resetCache() (clear search cache after import)
├─ SearchServer.checkBackupTable()
└─ SearchServer.restoreUserRecords()
↓
SetupImView.refreshButtonState() (UI update)
User receives or opens .limedb zipped database file:
External App (shares .limedb file)
↓
IntentHandler.processIntent() (validates intent/file)
├─ Extract URI and file info
├─ Validate file type (application/zip) and extension
├─ Convert InputStream to File
└─ handleLimedbImport()
↓
Extract table name from filename:
├─ If table is "related": Use importZippedDbRelated() path
└─ If table is IM table: Use importZippedDb() path with validation
↓
For IM tables - Check target table:
├─ If table has data: Single AlertDialog (same strings as 1a)
│ ├─ Cancel
│ ├─ Restore (restoreUserRecords = true)
│ └─ Don't Restore (restoreUserRecords = false)
└─ If table empty: Proceed directly (restoreUserRecords = false)
↓
SetupImFragment.importZippedDb(restoreUserRecords)
↓
SetupImController.importZippedDb(restoreUserRecords)
├─ If IM table:
│ ├─ SearchServer.isValidTableName() (validation for IM tables)
│ ├─ SearchServer.backupUserRecords() (if restoreUserRecords)
│ ├─ DBServer.importZippedDb() (database import)
│ ├─ SearchServer.resetCache() (clear search cache after import)
│ └─ SearchServer.restoreUserRecords() (if restoreUserRecords = true)
└─ If related table:
├─ DBServer.importZippedDbRelated() (related table import)
└─ SearchServer.resetCache() (clear search cache after import)
↓
SetupImView.refreshButtonState() (UI update)
User clicks IM buttons in SetupImFragment to initiate import. The flow starts with a dialog:
SetupImView (user clicks IM button - e.g., Array, CJ, Pinyin)
↓
SetupImFragment.onClick()
↓
SetupImLoadDialog.newInstance(tableName) (shows dialog)
├─ Display local file import button
├─ Display download buttons (with different options)
└─ Restore learning checkbox
User then has two choices in SetupImLoadDialog:
User clicks "Custom" button and selects local .limedb file:
SetupImLoadDialog (user clicks Custom/local file button)
↓
selectMappingFile() - Launch ACTION_OPEN_DOCUMENT
↓
User selects .limedb file
↓
SetupImLoadDialog.handleFileSelection()
↓
SetupImFragment.importDb()
↓
SetupImController.importZippedDb()
├─ MainActivityView.showProgressDialog()
├─ DBServer.importZippedDb() or importZippedDbRelated()
├─ SearchServer.resetCache()
└─ MainActivityView.hideProgressDialog()
↓
SetupImController.refreshMenu()
NavigationDrawerView.updateMenuItems()
SetupImFragment.initialImButtons() (refresh button states)
User clicks "Custom" button and selects local .cin or .lime file:
SetupImLoadDialog (user clicks Custom/local file button)
↓
selectMappingFile() - Launch ACTION_OPEN_DOCUMENT
↓
User selects .cin/.lime file
↓
SetupImLoadDialog.handleFileSelection()
├─ Check restore learning checkbox
└─ SetupImFragment.importTxtTable()
↓
SetupImController.importTxtTable()
├─ SearchServer.isValidTableName() (validation)
├─ MainActivityView.showProgressDialog()
├─ SearchServer.backupUserRecords() (if restore enabled)
├─ DBServer.importTxtTable() (file import)
├─ SearchServer.checkBackupTable()
├─ SearchServer.restoreUserRecords()
└─ MainActivityView.hideProgressDialog()
↓
SetupImController.refreshMenu()
SetupImFragment.initialImButtons() (refresh button states)
User clicks one of the download buttons (e.g., "15,945 from Phonetic Big5"):
SetupImLoadDialog (user clicks download button)
↓
SetupImLoadDialog.downloadAndImportZippedDb()
├─ Check network availability
├─ Check restore learning checkbox
└─ SetupImFragment.downloadAndImportZippedDb()
↓
SetupImController.downloadAndImportZippedDb()
├─ MainActivityView.showProgressDialog()
├─ LIMEUtilities.downloadRemoteFile() (download from cloud URL)
├─ NavigationDrawerView.onProgress() (show download %)
├─ DBServer.importZippedDb() (import downloaded file)
├─ SearchServer.resetCache()
├─ SearchServer.checkBackupTable()
├─ SearchServer.restoreUserRecords() (if restore enabled)
└─ MainActivityView.hideProgressDialog()
↓
SetupImController.refreshMenu()
NavigationDrawerView.updateMenuItems()
SetupImFragment.initialImButtons() (refresh button states)
This is the opposite of import - exporting IM and related data for sharing with other apps.
Common start:
SetupImView (user clicks Share To button)
↓
ShareDialog pops up
├─ Display list of IMs (Array, CJ, Pinyin, etc.)
├─ Display Related table option
└─ User selects IM or Related
↓
User selects an IM table in ShareDialog:
User clicks IM button (e.g., Array) → Format selection dialog → Chooses .limedb:
ShareDialog (user clicks IM button - e.g., Array)
↓
AlertDialog pops up (export format selection)
├─ "Export as Zipped Database (.limedb)" button
└─ "Export as Text Table (.lime)" button
↓
User selects .limedb option
↓
ShareManager.exportAndShareImTable(tableName)
├─ ProgressDialogManager.show()
├─ DBServer.exportZippedDb() (create zipped database file)
├─ ProgressDialogManager.dismiss()
└─ Create share intent
↓
System share dialog (user selects target app to share)
User clicks IM button (e.g., Array) → Format selection dialog → Chooses .lime:
ShareDialog (user clicks IM button - e.g., Array)
↓
AlertDialog pops up (export format selection)
├─ "Export as Zipped Database (.limedb)" button
└─ "Export as Text Table (.lime)" button
↓
User selects .lime option
↓
ShareManager.shareImAsText(tableName)
├─ ProgressDialogManager.show()
├─ SearchServer.getAllImKeyboardConfig() (get IM data)
├─ SearchServer.exportTxtTable() (export Im as text)
├─ ProgressDialogManager.dismiss()
└─ Create share intent
↓
System share dialog (user selects target app to share)
User selects Related in ShareDialog (only .limedb option available):
ShareDialog (user clicks Related button)
↓
AlertDialog pops up (export format selection)
└─ "Export as Zipped Database (.limedb)" button only
↓
User selects .limedb option (only choice)
↓
ShareManager.shareRelatedAsDatabase()
├─ ProgressDialogManager.show()
├─ DBServer.exportZippedDbRelated() (create zipped database file)
├─ ProgressDialogManager.dismiss()
└─ Create share intent
↓
System share dialog (user selects target app to share)
SetupImView (user clicks Backup button)
↓
SetupImFragment.backupLocalDrive()
├─ Launch ACTION_CREATE_DOCUMENT intent (file picker)
└─ User selects save location
↓
ActivityResultLauncher callback (backupLauncher)
↓
SetupImFragment.performBackup(uri)
↓
SetupImController.performBackup() (complete workflow)
├─ SearchServer.backupUserRecords(CUSTOM) (backup user records)
├─ MainActivityView.showProgressDialog()
└─ DBServer.backupDatabase(uri)
├─ Backup shared preferences
├─ Hold and close database connection
├─ LIMEUtilities.zip() (zip database files)
├─ Copy zip to user-selected URI
├─ Reopen database connection
└─ Show notification
↓
MainActivityView.hideProgressDialog()
SetupImView (user clicks Restore button)
↓
SetupImFragment.restoreLocalDrive()
├─ Launch ACTION_GET_CONTENT intent (file picker)
└─ User selects backup file
↓
ActivityResultLauncher callback (restoreLauncher)
↓
SetupImFragment.performRestore(uri)
↓
SetupImController.performRestore() (complete workflow)
├─ MainActivityView.showProgressDialog()
└─ DBServer.restoreDatabase(uri)
├─ Copy URI content to temp file
├─ Hold and close database connection
├─ LIMEUtilities.unzip() (extract to data directory)
├─ Restore shared preferences
├─ Reopen database connection
├─ SearchServer.resetCache()
├─ Check and update related table
└─ Show notification
↓
MainActivityView.hideProgressDialog()
SetupImController.refreshMenu()
NavigationDrawerView.updateMenuItems() (menu refresh)
Each component has a clearly defined purpose:
- Controller: Orchestrate operations, coordinate between view and model
- Manager: Handle specialized UI operations (progress, sharing, navigation)
- Handler: Process specific event types (intents, callbacks)
- View: Display data and receive user input
- Model: Provide data access without UI concerns
Controllers and managers receive dependencies through constructors:
// Constructor explicitly shows dependencies
public SetupImController(DBServer dbServer, SearchServer searchServer) {
this.dbServer = dbServer;
this.searchServer = searchServer;
}Views implement only the interfaces they need:
// SetupImFragment implements only SetupImView
public class SetupImFragment extends Fragment implements SetupImView {
// Receives callbacks only for button states and IM list
}
// ManageImFragment implements only ManageImView
public class ManageImFragment extends Fragment implements ManageImView {
// Receives callbacks only for records and record count
}Views depend on abstractions (interfaces) not concrete classes:
// View depends on interface, not concrete controller
SetupImView view = fragment;
controller.setSetupImView(view); // Pass interface reference
// Manager depends on interface, not concrete view
ProgressDialogManager progressMgr; // Abstract operationControllers and managers return safe defaults instead of null:
// Return empty config list instead of null
public List<ImConfig> getImAllConfigList(String table) {
List<ImConfig> configs = searchServer.getImConfigs(table);
return configs != null ? configs : Collections.emptyList();
}- Clear separation of concerns improves maintainability
- Consistent patterns across components
- Easier to locate and understand code
- Reduced code duplication through reuse
- Controllers can be unit tested without Android context
- Mock implementations of interfaces for testing
- No direct dependencies on Activities/Fragments
- Predictable behavior and error handling
- Changes to business logic isolated to controllers
- UI updates isolated to view implementations
- Model layer remains stable and independent
- Clear interfaces define expected behavior
- Easy to add new features by adding controllers
- Reuse existing controllers and managers
- Extend without modifying existing code
- Support for multiple UI layers (if needed)
- Consistent error handling and feedback
- Progress dialogs for long operations
- Responsive UI through controller delegation
- Predictable navigation and state management
Controllers:
- ✅ BaseController (abstract base)
- ✅ SetupImController (navigation, setup, backup/restore, button states)
- ✅ ManageImController (record CRUD, search, related phrases)
Handlers/Managers:
- ✅ IntentHandler (external file imports)
- ✅ ProgressDialogManager (progress dialogs)
- ✅ ShareManager (export and share operations)
- ✅ NavigationManager (fragment navigation)
View Interfaces:
- ✅ ViewUpdateListener (base)
- ✅ MainActivityView (main activity operations)
- ✅ SetupImView (setup fragment operations)
- ✅ ManageImView (manage fragment operations)
- ✅ ManageRelatedView (related phrases operations)
- ✅ NavigationDrawerView (navigation operations)
Refactored Views:
- ✅ MainActivity (delegates to controllers/managers)
- ✅ NavigationDrawerFragment (uses NavigationManager)
- ✅ SetupImFragment (uses SetupImController)
- ✅ ManageImFragment (uses ManageImController)
- ✅ ManageRelatedFragment (uses ManageImController)
- ✅ ImportDialog (uses ManageImController)
- ✅ ShareDialog (uses ShareManager)
- ✅ LIMEPreference + PrefsFragment (preference screen)
The implemented architecture achieves all design goals:
- ✅ Separation of Concerns: View, Controller/Handler/Manager, and Model layers are clearly separated
- ✅ Testability: Controllers are standalone and testable
- ✅ Maintainability: Responsibilities are clearly defined
- ✅ Reusability: Shared controller instances across views
- ✅ Code Organization: Consistent patterns throughout
- ✅ Data Integrity: All model access through controllers
Data Flow: View → Controller/Handler/Manager → Model → Database ✅
Build Status: All builds successful ✅
Document Version: 1.1
Architecture Version: 1.1
Status: Complete and Implemented with Threading Safety
Last Updated: December 26, 2025
December 26, 2025 - Major Architecture Cleanup:
-
File Relocation:
- Moved
IntentHandler.javafromui/handler/toui/level - Moved
NavigationManager.javafromui/handler/toui/level - Moved
ProgressManager.javafromui/handler/toui/level - Moved
ShareManager.javafromui/handler/toui/level - Updated all imports across the project
- Moved
-
Threading Safety Enhancements:
- Added thread-safe UI operation methods to
BaseController:handleError(view, message, exception)showProgress(view, message)hideProgress(view)updateProgress(view, percentage, status)showToast(view, message, length)
- All methods post to main thread via
mainHandler - Fixed
ManageImControllerthreading issue: wrapped UI calls inmainHandler.post()
- Added thread-safe UI operation methods to
-
Callback Refactoring:
- Moved
NavigationDrawerCallbacksimplementation fromMainActivitytoNavigationManager NavigationManagernow implementsNavigationDrawerFragment.NavigationDrawerCallbacks- Added method:
onNavigationDrawerItemSelected(int position) - Removed
onNavigationDrawerItemSelected()from MainActivity SetupImControllernow implementsImportDialog.OnImportIMSelectedListener- Added method:
onImportDialogImSelected(String tableName, boolean restoreUserRecords) - Added method:
setFileToImport(File file) - Removed import callback from MainActivity
- Moved
-
API Additions:
MainActivity.getNavigationManager()- getter for NavigationManager- Added comprehensive javadoc to MainActivity