Revamping a Mobile Video Editor: MVVM + UDF Architecture and Redo/Undo Design
This article details the comprehensive redesign of a mobile video‑editing page, covering background challenges, requirement analysis, MVVM + UDF architectural choices, module decomposition, dependency‑injection implementation, and the design of a robust Redo/Undo system to improve maintainability and user experience.
1. Background
In the era of digital content explosion, mobile devices have become a crucial arena for video creation. The video‑editing page, as the core scenario of the creation tool, offers creators rich expressive means and a creative platform, while improving production efficiency through an intuitive UI and integrated features such as media, audio, effects, and text.
However, the page’s design and implementation are complex: dozens of modules interweave horizontally, and each module provides extensive functionality vertically, raising high demands on page architecture and code design.
2. Clarify Requirements
The initial version of the video‑editing page has been in production for a long time without major upgrades, suffering from issues like excessive tracks, redundant information, high user comprehension cost, outdated main‑track design, and inconvenient track operations.
The product team aims to perform a business upgrade centered on lightweight editing, redesigning layout, interaction, optimizing existing features, and adding necessary new functions.
3. Requirement Analysis
The upgrade focuses on four aspects:
Layout: Redefine interaction zones, redesign the main page and operation panel visuals.
User Interaction: Design new interactions for core controls such as video tracks, multi‑track media, toolbars, and quick editing.
Feature Optimization: Upgrade and multi‑track refactor existing functions like clipping, text, stickers, transitions, and music.
New Features: Add Redo/Undo, quick text editing, frame editing, full‑screen preview, etc.
3.1 What the Business Upgrade Entails
Key points are layout redesign, interaction overhaul, feature optimization, and new feature addition.
3.2 Current Code Situation
The existing codebase struggles to support such a large‑scale upgrade:
Page Architecture: The old UI lacks a clear architecture, mixing MVC and MVP, with most logic residing in Activities and Fragments, some exceeding 5,000 lines, making maintenance difficult.
Business Modules: Functionality is split between a main Activity and multiple Fragments, communicating via direct object references, resulting in heavy coupling.
Gray‑release Requirements: Modifying the old code for a gradual rollout would be disastrous; copying code would not solve the underlying problems and would further degrade maintainability.
3.3 Key Issues to Consider
We decide to rebuild the page using an MVVM + UDF architecture, ensuring compatibility with existing panels, designing a low‑intrusion Redo/Undo mechanism, and handling the complex track control.
4. Specific Solution
4.1 Page Architecture Design
4.1.1 Design Principles
After choosing MVVM + UDF, the following principles guide the design:
Adopt MVVM for overall architecture.
Drive UI from a single source of truth (UiState).
Follow the Single Source of Truth (SSOT) principle.
Enforce Unidirectional Data Flow (UDF).
Modularize business logic to increase cohesion and reduce coupling.
Use dependency injection to improve reuse and manage object lifecycles.
Below is a simplified architecture diagram:
4.1.2 Layered Design
The architecture is split vertically into UI Layer, Domain Layer, and Data Layer.
UI Layer: Consists of Activities/Fragments and ViewModels. UI state is represented by immutable UiState objects, optionally aggregated by UiStateHolder.
data class PlayBtnUiState(
val isShowPlayView: Boolean, // play button visibility
val isPlaying: Boolean, // play/pause state
val onPlayViewClick: (() -> Unit)? // click callback
)LiveData/StateFlow mediates data flow; UI observes UiState changes to update the view.
val playBtnUiState = MutableLiveData<PlayBtnUiState>()
stateHolder.playBtnUiState.observe(owner) { uiState ->
updatePlayViewUiState(uiState)
}Domain Layer: Encapsulates complex business logic using UseCases, enabling reuse across features.
Data Layer: Composed of Repositories and Data Sources, adapted to fit the existing data acquisition patterns while applying recommended architectural improvements.
4.1.3 Module Splitting
Horizontal modules are divided across UI and Domain layers, resulting in clear responsibilities for each feature such as clipping, subtitles, stickers, and music.
4.1.4 Unidirectional Data Flow (UDF)
State (UiState) flows from the data layer to the UI, while events flow from the UI to the data layer, ensuring consistency, testability, and maintainability.
4.1.5 Dependency Injection (DI)
We use Hilt for DI, selecting appropriate scopes (e.g., @ActivityScoped, @ViewModelScoped) to manage object lifecycles.
@HiltViewModel
class EditorStickerListViewModel @Inject (
private val projectRepository: ProjectRepository,
private val streamingRepository: StreamingRepository,
private val stickerAddUseCase: EditorStickerAddUseCase,
private val materialTrackUseCase: TrackCommonUseCase
)The ViewModel injects repositories and use‑cases to handle sticker addition:
fun addStickerMaterials(dataList: List<MaterialItem>) {
// 1. Convert material list to sticker clip list
val stickerClipList = createStickerBClipList(dataList)
// 2. Add clips to project data
projectRepository.addStickerClips(stickerClipList)
// 3. Insert clips into rendering engine
streamingRepository.insertStickerClipList(insertTime, stickerClipList)
// 4. Refresh track UI
materialTrackUseCase.refreshMultiMaterialTrack()
// 5. Refresh engine time
streamingRepository.refreshCurrentTime()
}4.2 Design and Implementation of Specific Features
4.2.1 Compatibility with Legacy Panels
During the transition, the new editor must support legacy panels. We abstract over 100 legacy functions into interfaces, implement them in the old Activity, and route calls from the new UI through a proxy layer, allowing gradual gray‑release control.
4.2.2 Bottom Panel Component
All functional panels are embedded as Fragments. A base class EditorBaseFragment defines common behavior, while a manager handles addition, removal, animations, and stack management. Panels are presented in three modal heights (half, full, fullscreen).
enum class FragmentContainerModal {
/** 半模态-露出部分轨道 */
MODAL_HALF,
/** 全模态-遮挡整个操作区域 */
MODAL_ALL,
/** 全屏模态-容器为全屏 */
MODAL_FULL_SCREEN
}4.2.3 Video Track Control
The track control combines custom Canvas drawing with a few Views, managing state, rendering order, event dispatch, animation, and performance optimizations such as on‑screen drawing only and two‑level frame caching.
4.2.4 Redo/Undo Design
We adopt a snapshot (memento) approach: the entire editing project state is backed up, stored in a two‑level cache (memory + disk). A sliding window with a doubly‑linked list keeps recent snapshots, enabling undo/redo operations.
Although the current implementation compromises on performance and memory usage, the MVVM + UDF architecture naturally aligns with a command‑based approach, which would be a more optimal solution.
5. Summary
The video‑editing page, a high‑complexity single page, underwent a six‑month, 90k‑line code overhaul involving multiple teams. The redesign demonstrates that continuous, incremental refactoring is essential, while large‑scale architectural upgrades should be tied to major feature changes to ensure feasibility and ROI. Simpler, principle‑driven designs that focus on core problems yield more maintainable and testable code.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Bilibili Tech
Provides introductions and tutorials on Bilibili-related technologies.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
