File: NativeTermFormFragment.kt
Lines: 3,347 lines
Complexity: High - UI, business logic, and network code mixed together
NativeTermFormFragment (UI)
↓
NativeTermFormViewModel (Business Logic)
↓
TermFormRepository (Data Layer)
↓
TermFormUtils (Pure Functions)
Role: UI presentation layer
Responsibilities:
- View lifecycle management
- View binding setup and cleanup
- UI event handling (button clicks, text changes)
- Observing ViewModel state
- Displaying popups and dialogs
- Navigation and dismissal
What to Keep (move from original):
- Dialog creation (lines 101-114)
- View binding (lines 32-34)
- UI setup methods (simplified)
- Button click listeners
- Text watchers
- Popup display logic
- Status button creation/update
What to Remove:
- All network calls
- All data parsing
- All business logic
- State management
Role: Business logic coordinator
Responsibilities:
- State management with StateFlow/LiveData
- Coordinating user actions
- Managing parent term list and data map
- Managing linking state
- Validating form state
- Calling repository methods
- Exposing state to UI
State:
data class TermFormState(
val termId: Int? = null,
val termText: String = "",
val translation: String = "",
val status: Int = 1,
val parents: List<String> = emptyList(),
val isLinked: Boolean = false,
val isParentTermForm: Boolean = false,
val parentTermDataMap: Map<String, TermFormData> = emptyMap(),
val isLoading: Boolean = false,
val error: String? = null,
val isAiEnabled: Boolean = false
)Methods (move from Fragment):
updateTranslation(text: String)updateStatus(status: Int)toggleLinking()addParent(parentText: String)removeParent(parentText: String)saveTerm()fetchParentData(parent: String)updateStatusBasedOnParent()sendToAi(term: String, sentence: String)getLanguageName(languageId: Int)validateForm()
Role: All data operations
Responsibilities:
- Network calls to Lute server
- API requests (save, search, fetch)
- HTML parsing and data extraction
- AI API calls
- Caching responses
- Error handling for network operations
Methods (extract from Fragment):
Term Operations:
suspend fun saveTerm(termData: TermFormData): Result<Unit>
suspend fun fetchTermDataById(termId: Int): Result<TermFormData>
suspend fun searchTerms(query: String, languageId: Int): Result<List<String>>Parent Term Operations:
suspend fun fetchParentTermData(parent: String, languageId: Int): Result<TermFormData>
suspend fun searchParentTerms(query: String, languageId: Int): Result<List<String>>
suspend fun checkParentExists(parent: String, languageId: Int): Result<Boolean>
suspend fun createParentTerm(parent: String, languageId: Int): Result<Unit>Server Operations:
suspend fun testServerConnection(serverUrl: String): Boolean
suspend fun getLanguageInfo(languageId: Int): Result<LanguageInfo>AI Operations:
suspend fun requestAiTranslation(term: String, sentence: String, language: String): Result<String>Data Parsing (delegate to Utils):
fun parseTermDataFromHtml(html: String, termId: Int, parent: String): TermFormData
fun parseLanguageNameFromHtml(html: String): StringRole: Pure utility functions
Responsibilities:
- Validation functions
- Color utilities
- Status mappings
- HTML parsing helpers
- String utilities
Methods (extract from Fragment):
Validation:
fun isTermTextValidModification(original: String, modified: String): Boolean
fun validateParents(parents: List<String>): List<String>Colors & Styles:
fun getStatusColors(): Map<Int, String>
fun getContrastColor(color: Int): Int
fun getStatusInfo(statusId: Int): StatusInfoHTML Parsing:
fun parseJsonArray(content: String): List<String>
fun parseTermIdFromSearchResults(content: String, parent: String): Int?Data Classes:
data class StatusInfo(val id: Int, val label: String, val color: String)
data class LanguageInfo(val id: Int, val name: String)Tasks:
-
Create
TermFormUtils.ktinui/nativeread/Term/ -
Extract and move utility functions:
isTermTextValidModification()(line 1249)getContrastColor()(line 1141)- Status color maps (lines 953-962, 2649-2658)
StatusInfodata class (line 2335)- JSON parsing helpers (lines 501-517, 1709-1745, 2350-2384)
-
Update Fragment to use Utils
-
Run tests to ensure no breakage
Files:
ui/nativeread/Term/TermFormUtils.kt
Estimated Time: 2 days
Tasks:
-
Create
TermFormRepository.ktinui/nativeread/Term/ -
Extract and move all network operations:
saveTermToServer()(lines 1255-1551)testServerAndSaveTerm()(lines 1557-1586)performTermSearch()(lines 430-499)performParentTermSearch()(lines 720-789)fetchTermDataById()(lines 2386-2475)searchAndFetchParentTermData()(lines 1979-2348)fetchParentTermData()(lines 1897-1916)checkAndAddParentTerm()(lines 1605-1706)createNewTerm()(lines 1767-1875)sendTermToAi()(lines 3017-3154)getLanguageNameById()(lines 3201-3285)
-
Convert to suspend functions and use Result
-
Add proper error handling
-
Implement simple caching (in-memory)
Files:
ui/nativeread/Term/TermFormRepository.kt
Estimated Time: 7 days
Tasks:
-
Create
NativeTermFormViewModel.ktinui/nativeread/Term/ -
Define
TermFormStatedata class -
Move state management from Fragment:
termFormData,storedTermData,parentTermDataMapselectedStatus,isLinked- Parent buttons list, status buttons list
-
Create StateFlow for each state field
-
Expose methods for user actions:
updateTranslation(),updateStatus(),toggleLinking()addParent(),removeParent()saveTerm(),fetchParentData()sendToAi(),getLanguageName()
-
Integrate with Repository
-
Implement validation logic
Files:
ui/nativeread/Term/NativeTermFormViewModel.ktui/nativeread/Term/TermFormState.kt
Estimated Time: 7 days
Tasks:
-
Add ViewModel to Fragment
-
Setup observers for StateFlow
-
Remove all state variables (moved to ViewModel)
-
Remove all network calls (moved to Repository)
-
Remove all business logic (moved to ViewModel)
-
Keep only:
- View binding
- UI setup
- Click listeners (call ViewModel methods)
- Popup display
- Lifecycle management
-
Simplify methods:
setupUI()- just setup views, no logic- Button listeners - call ViewModel methods
- Update UI based on observed state
-
Test all user flows
Estimated Time: 8 days
Tasks:
-
Write unit tests for Repository:
- Test network operations with mock server
- Test error handling
- Test caching
-
Write unit tests for ViewModel:
- Test state changes
- Test business logic
- Test error handling
-
Write unit tests for Utils:
- Test validation functions
- Test parsing functions
-
Integration tests:
- Test Fragment with mock ViewModel
- Test end-to-end flows
-
Clean up:
- Remove dead code
- Add KDoc comments
- Format code
Files:
test/.../TermFormRepositoryTest.kttest/.../NativeTermFormViewModelTest.kttest/.../TermFormUtilsTest.kt
Estimated Time: 7 days
ui/nativeread/Term/
├── NativeTermFormFragment.kt (1,200 lines - UI)
├── NativeTermFormViewModel.kt (1,000 lines - Logic)
├── TermFormRepository.kt (600 lines - Data)
├── TermFormUtils.kt (300 lines - Utilities)
├── TermFormState.kt (50 lines - Data class)
├── TermFormData.kt (existing)
├── TermData.kt (existing)
├── TermDataExtractor.kt (existing)
└── ParentTermSuggestionsAdapter.kt (existing)
NativeTermFormFragment
↓ observes
NativeTermFormViewModel
↓ calls
TermFormRepository
↓ uses
TermFormUtils (parsing, validation)
// Before (in Fragment)
private fun isTermTextValidModification(original: String, modified: String): Boolean {
return original.equals(modified, ignoreCase = true)
}
// After
// TermFormUtils.kt
object TermFormUtils {
fun isTermTextValidModification(original: String, modified: String): Boolean {
return original.equals(modified, ignoreCase = true)
}
fun getStatusColors(): Map<Int, String> = mapOf(
1 to "#b46b7a",
2 to "#BA8050",
// ...
)
fun getContrastColor(color: Int): Int {
val luminance = (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255
return if (luminance > 0.5) Color.BLACK else Color.WHITE
}
}
// In Fragment
private fun isTermTextValidModification(original: String, modified: String): Boolean {
return TermFormUtils.isTermTextValidModification(original, modified)
}// Before (in Fragment)
private fun saveTermToServer(termData: TermFormData?, callback: (Boolean) -> Unit) {
Thread {
// Network logic...
activity?.runOnUiThread { callback(true) }
}.start()
}
// After
// TermFormRepository.kt
class TermFormRepository(
private val context: Context,
private val serverSettings: ServerSettingsManager
) {
suspend fun saveTerm(termData: TermFormData): Result<Unit> = withContext(Dispatchers.IO) {
try {
val serverUrl = serverSettings.getServerUrl()
val connection = URL("$serverUrl/read/edit_term/${termData.termId}")
.openConnection() as HttpURLConnection
// ... network logic
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}
// In ViewModel
fun saveTerm() = viewModelScope.launch {
_isLoading.value = true
repository.saveTerm(termFormData.value)
.onSuccess { _isLoading.value = false }
.onFailure { _error.value = it.message }
}// Before (in Fragment)
private var selectedStatus = 0
private var isLinked = false
// After
// NativeTermFormViewModel.kt
class NativeTermFormViewModel(
private val repository: TermFormRepository,
private val termFormData: TermFormData,
private val isParentTermForm: Boolean
) : ViewModel() {
private val _state = MutableStateFlow(TermFormState(
termId = termFormData.termId,
termText = termFormData.termText,
translation = termFormData.translation,
status = termFormData.status,
parents = termFormData.parents,
isLinked = termFormData.isLinked
))
val state: StateFlow<TermFormState> = _state.asStateFlow()
fun updateStatus(status: Int) {
_state.update { it.copy(status = status) }
}
fun toggleLinking() {
_state.update { it.copy(isLinked = !it.isLinked) }
}
fun saveTerm() {
viewModelScope.launch {
val currentState = _state.value
repository.saveTerm(currentState.toTermFormData())
.onSuccess { /* handle success */ }
.onFailure { /* handle error */ }
}
}
}// Before
class NativeTermFormFragment : DialogFragment() {
private var selectedStatus = 0
private var isLinked = false
private var parentTermDataMap: MutableMap<String, TermFormData> = mutableMapOf()
private fun saveTerm() {
// Business logic...
saveTermToServer(termData) { success -> ... }
}
}
// After
class NativeTermFormFragment : DialogFragment() {
private lateinit var viewModel: NativeTermFormViewModel
private var _binding: DialogNativeTermFormBinding? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViewModel()
setupObservers()
setupUI()
}
private fun setupObservers() {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.state.collect { state ->
updateUI(state)
}
}
}
private fun setupUI() {
binding.saveButton.setOnClickListener {
viewModel.saveTerm()
}
}
private fun updateUI(state: TermFormState) {
binding.translationText.setText(state.translation)
updateStatusButtons(state.status)
updateParentButtons(state.parents)
updateLinkButton(state.isLinked)
}
}| Metric | Before | After | Improvement |
|---|---|---|---|
| Fragment lines | 3,347 | 1,200 | 64% ↓ |
| Total classes | 1 | 4 | Practical split |
| Cyclomatic complexity | High | Medium | ~50% ↓ |
| Testability | Low | High | High |
| Network operations in UI | Yes | No | Clean |
| Business logic in UI | Yes | No | Clean |
- Manageable: Each file has a clear, focused purpose
- Testable: Repository and ViewModel can be unit tested
- Maintainable: Changes localized to specific layer
- Understandable: Linear flow: UI → ViewModel → Repository
- Not overkill: Only 4 classes, not 9
- Reusability: Repository can be used by other screens
- Type-safe: Result instead of callbacks
| Risk | Mitigation |
|---|---|
| Breaking existing functionality | Comprehensive testing in Phase 5 |
| State synchronization issues | Single source of truth in ViewModel |
| Performance regression | Benchmark before/after |
| Learning curve | Clear documentation and examples |
| Week | Phase | Duration |
|---|---|---|
| 1 | TermFormUtils | 2 days |
| 2-3 | TermFormRepository | 7 days |
| 4-5 | ViewModel | 7 days |
| 6-7 | Fragment refactor | 8 days |
| 8 | Testing & cleanup | 7 days |
Total: 8 weeks (31 working days)
- New files: 4 (Repository, ViewModel, State, Utils)
- Total classes in Term package: ~15 (from ~11)
- Not overwhelming: Clear organization
- Apply similar pattern to
SentenceReadFragment(3,074 lines) - Apply pattern to
MainActivity(2,396 lines) - Consider creating a
BaseRepositoryfor common network code - Consider creating a
BaseViewModelfor common ViewModel patterns