Agent SkillsAgent Skills
KrystianYCSilva

zk-mvc-to-mvvm-migration

@KrystianYCSilva/zk-mvc-to-mvvm-migration
KrystianYCSilva
0
0 forks
Updated 4/13/2026
View on GitHub

Guia migração de ZK MVC (Composers) para MVVM (ViewModels) passo-a-passo, mostrando redução de boilerplate e ganhos em testabilidade. Use quando: modernizar sistemas legados ZK 3.x-8.x, treinar equipes em MVVM, ou planejar refatoração de Composers para ViewModels.

Installation

$npx agent-skills-cli install @KrystianYCSilva/zk-mvc-to-mvvm-migration
Claude Code
Cursor
Copilot
Codex
Antigravity

Details

Path.opencode/skills/zk-mvc-to-mvvm-migration/SKILL.md
Branchmaster
Scoped Name@KrystianYCSilva/zk-mvc-to-mvvm-migration

Usage

After installing, this skill will be available to your AI coding assistant.

Verify installation:

npx agent-skills-cli list

Skill Instructions


name: zk-mvc-to-mvvm-migration description: | Guia migração de ZK MVC (Composers) para MVVM (ViewModels) passo-a-passo, mostrando redução de boilerplate e ganhos em testabilidade. Use quando: modernizar sistemas legados ZK 3.x-8.x, treinar equipes em MVVM, ou planejar refatoração de Composers para ViewModels.

ZK MVC to MVVM Migration

Migração de padrão MVC (GenericForwardComposer) para MVVM (@ViewModel, @Command) reduz complexidade ciclomática em ~40% e elimina acoplamento com componentes UI.

Comparação MVC vs MVVM

MVC com Composer (Legado)

public class LoginComposer extends GenericForwardComposer {
    private Textbox username;
    private Textbox password;
    private Button loginBtn;
    
    public void onClick$loginBtn() throws Exception {
        // Acoplado aos componentes UI
        String user = username.getValue();
        String pass = password.getValue();
        
        if (authenticator.authenticate(user, pass)) {
            Sessions.getCurrent().setAttribute("user", user);
            Executions.sendRedirect("/layout.zul");
        } else {
            Messagebox.show("Usuário/senha inválidos!", "Erro", 
                           Messagebox.OK, Messagebox.ERROR);
        }
    }
}

Problemas:

  • Acoplamento forte com componentes ZK (Textbox, Button)
  • Dificil de testar unitariamente
  • Mistura lógica de negócio com manipulação de UI
  • Boilerplate de getters/setters de componentes

MVVM com ViewModel (Moderno)

@ViewModel
public class LoginViewModel {
    
    private String username;
    private String password;
    
    @Command
    public void doLogin() {
        if (authenticator.authenticate(username, password)) {
            Sessions.getCurrent().setAttribute("user", username);
            Executions.sendRedirect("/layout.zul");
        }
    }
    
    // Getters/setters padrão
}

Benefícios:

  • POJO simples, sem dependência de UI
  • Testável com JUnit puro
  • Separação clara de responsabilidades
  • Menos código (~15 linhas vs ~50)

Como migrar passo-a-passo

Passo 1: Identificar dados bound

No arquivo .zul MVC, identifique quais componentes usam $component:

<!-- MVC -->
<textbox id="username" />
<textbox id="password" />
<button label="Login" onClick="$composer.onClick$loginBtn()" />

Estes tornam-se propriedades no ViewModel:

private String username;
private String password;

Passo 2: Extrair lógica para commands

Mova a lógica dos métodos onClick$X() para métodos anotados com @Command:

Antes (MVC):

public void onClick$btnSalvar(ComponentEvent event) {
    funcionario.setNome(txtNome.getValue());
    funcionario.setEmail(txtEmail.getValue());
    service.salvar(funcionario);
    Clients.showNotification("Salvo!");
    listbox.removeAllChildren();
    carregarLista();
}

Depois (MVVM):

@Command
@NotifyChange({"funcionario", "mensagem"})
public void salvar() {
    service.salvar(funcionario);
    this.mensagem = "Salvo com sucesso!";
    carregarFuncionarios();
}

Passo 3: Substituir acesso a componentes

Substitua component.getValue() por propriedades diretas:

MVC PatternMVVM Pattern
txtNome.getValue()nome (property direta)
txtEmail.setValue(x)setEmail(x)
listbox.getModel()model (property)
Clients.showNotification(msg)Injete via @Wire ou use binding

Passo 4: Atualizar bindings na view

Mude de $composer para vm:

<!-- ANTES (MVC) -->
<window composerClass="br.com.LoginComposer">
    <textbox id="username" value="@{composer.username}" />
    <button onClick="$composer.onClick$loginBtn()" />
</window>

<!-- DEPOIS (MVVM) -->
<window viewModel="br.com.LoginViewModel">
    <textbox value="@bind(vm.username)" />
    <button label="Login" onClick="@command('doLogin')" />
</window>

Passo 5: Migrar inicialização

Substitua doAfterCompose() por @Init:

MVC:

@Override
public void doAfterCompose(Component comp) throws Exception {
    super.doAfterCompose(comp);
    carregarDados();
}

MVVM:

@Init
public void init() {
    carregarDados();
}

Passo 6: Migrar notificações de mudança

Substitua atualizações manuais de UI por @NotifyChange:

MVC (atualização manual):

public void onClick$btnAtualizar() {
    listaModel.clear();
    listaModel.addAll(service.buscarTodos());
    // UI atualiza automaticamente via modelo
}

MVVM (notificação declarativa):

@Command
@NotifyChange("funcionarios")
public void atualizarLista() {
    this.funcionarios = service.buscarTodos();
    // UI atualiza automaticamente via binding
}

Como lidar com casos especiais

Wire de componentes (quando necessário)

Evite, mas se precisar acessar componente diretamente:

@ViewModel
public class MeuViewModel {
    
    @Wire  // Injeta componente da view
    private Listbox lista;
    
    @Command
    public void selecionarItem() {
        // Acesso direto (use apenas quando necessário)
        Listitem selected = lista.getSelectedItem();
    }
}

Na view:

<listbox id="lista" model="@load(vm.funcionarios)" />

Passar parâmetros para commands

Use @BindingParam:

@Command
public void editar(@BindingParam("id") Long id) {
    this.entidade = service.buscarPorId(id);
}
<button onClick="@command('editar', id=each.id)" />

Mensagens e notificações

Injete Messagebox ou use services:

@ViewModel
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class FuncionariosViewModel {
    
    private final NotificationService notificationService;
    
    @Autowired
    public FuncionariosViewModel(NotificationService ns) {
        this.notificationService = ns;
    }
    
    @Command
    public void salvar() {
        service.salvar(funcionario);
        notificationService.info("Registro salvo!");
    }
}

Como testar ViewModel migrado

ViewModels são POJOs, então teste com JUnit puro:

class LoginViewModelTest {
    
    private LoginViewModel viewModel;
    private MockAuthenticator mockAuth;
    
    @BeforeEach
    void setUp() {
        mockAuth = Mockito.mock(MockAuthenticator.class);
        viewModel = new LoginViewModel(mockAuth);
    }
    
    @Test
    void testDoLogin_UsuarioValido_Redireciona() {
        // Arrange
        Mockito.when(mockAuth.authenticate("admin", "secret"))
               .thenReturn(true);
        viewModel.setUsername("admin");
        viewModel.setPassword("secret");
        
        // Act
        viewModel.doLogin();
        
        // Assert
        assertEquals("admin", viewModel.getUsername());
        // Verifique redirecionamento via mock
    }
    
    @Test
    void testDoLogin_SenhaInvalida_MostraErro() {
        // Arrange
        Mockito.when(mockAuth.authenticate("admin", "wrong"))
               .thenReturn(false);
        
        // Act
        viewModel.doLogin();
        
        // Assert
        // Verifique que erro foi disparado
    }
}

Checklist de migração

  • Extrair todas as propriedades de componentes para fields
  • Converter métodos onClick$X() para @Command
  • Substituir acessos a componentes por properties
  • Adicionar @NotifyChange nos commands que modificam estado
  • Migrar doAfterCompose() para @Init
  • Atualizar bindings no .zul (@{composer.x}@bind(vm.x))
  • Remover extensão de GenericForwardComposer
  • Adicionar anotação @ViewModel (opcional)
  • Testar funcionalidade na UI
  • Criar testes unitários para o ViewModel

Métricas de sucesso

Após migração, verifique:

MétricaMVCMVVMMelhoria
Linhas de código~150~90-40%
Dependências UI5-10 classes ZK0-100%
TestabilidadeBaixa (mocks complexos)Alta (JUnit puro)+++
Complexidade ciclomática15-208-12-40%

Comparação detalhada MVC vs MVVM

1. MVC Pattern (Composer)

No MVC do ZK, o "Controller" é uma classe Java que estende SelectorComposer. Este Composer está diretamente ligado a uma página ZUL e é responsável por toda lógica de interação.

Como funciona:

  1. View (ZUL): A página ZUL é um layout de componentes UI. Cada componente que precisa ser manipulado recebe um id único. O componente <window> ou raiz usa o atributo apply para especificar sua classe Composer.
  2. Controller (Composer): A classe Java é o "cérebro":
    • Usa @Wire para obter referências diretas aos componentes UI da página ZUL, combinando nomes de variáveis com ids dos componentes
    • Usa @Listen ou convenções de nomenclatura (onClick$myButton) para lidar com eventos
    • Dentro dos handlers, o código do Composer manualmente puxa dados dos componentes (ex: myTextbox.getValue()) e manualmente empurra dados de volta

Características chave:

  • Pattern: Imperativo e orientado a eventos
  • Acoplamento: Alto - O Composer está fortemente acoplado à View
  • Testabilidade: Difícil - Requer ambiente complexo de teste de UI

2. MVVM Pattern (ViewModel)

MVVM promove separação de concerns mais limpa. O "ViewModel" é um POJO simples que expõe dados e commands; não tem conhecimento dos componentes UI.

Como funciona:

  1. View (ZUL): Define o layout. Em vez de apply, usa viewModel para linkar a classe ViewModel
  2. Data Binding: O BindComposer gerencia a conexão:
    • Propriedades dos componentes são ligadas declarativamente às propriedades do ViewModel usando @bind()
    • Eventos UI são ligados a métodos do ViewModel usando @command()
  3. ViewModel (POJO): Expõe estado através de getters/setters e comportamento através de métodos anotados com @Command

Características chave:

  • Pattern: Declarativo e data-centric
  • Acoplamento: Baixo - ViewModel é completamente desacoplado da View
  • Testabilidade: Fácil - POJO pode ser testado unitariamente sem dependências de UI

Tabela comparativa completa

AspectoMVC (Composer)MVVM (ViewModel)Recomendação
AcoplamentoAlto (Composer depende da View)Baixo (ViewModel independente)MVVM para manutenibilidade
TestabilidadeDifícil (requer contexto UI)Fácil (POJO puro)MVVM para qualidade
BoilerplateMais código para wiring/eventsMenos código, bindings declarativosMVVM para produtividade
LegibilidadeLógica dispersa em handlersLógica centralizada em commandsMVVM para clareza
Curva de aprendizadoLevemente mais fácil para iniciantesRequer entender data bindingMVVM é padrão moderno
ManutençãoDifícil (mudanças na View quebram Controller)Fácil (View e ViewModel evoluem separadamente)MVVM para longo prazo

Conclusão: Para qualquer novo projeto ZK, MVVM é altamente recomendado. Produz código mais limpo, manutenível e testável, alinhado com práticas modernas de desenvolvimento. MVC deve ser reservado para manutenção de legado.

Exemplo completo lado-a-lado

Login Form - MVC

login-mvc.zul:

<window title="Login MVC" border="normal" width="300px"
        apply="br.com.zkminsamples.controller.LoginComposer">
    <grid>
        <rows>
            <row>
                <label value="Usuário:"/>
                <textbox id="txtUsername" width="150px"/>
            </row>
            <row>
                <label value="Senha:"/>
                <textbox id="txtPassword" type="password" width="150px"/>
            </row>
            <row spans="2" align="center">
                <button id="btnLogin" label="Entrar"/>
            </row>
        </rows>
    </grid>
</window>

LoginComposer.java:

public class LoginComposer extends SelectorComposer<Component> {
    
    @Wire
    private Textbox txtUsername;
    
    @Wire
    private Textbox txtPassword;
    
    @Listen("onClick = #btnLogin")
    public void doLogin() {
        // Manualmente obtém valores dos componentes UI
        String username = txtUsername.getValue();
        String password = txtPassword.getValue();
        
        // ... lógica de autenticação ...
    }
}

Login Form - MVVM

login-mvvm.zul:

<window title="Login MVVM" border="normal" width="300px"
        viewModel="@id('vm') @init('br.com.zkminsamples.viewmodel.LoginViewModel')">
    <grid>
        <rows>
            <row>
                <label value="Usuário:"/>
                <textbox width="150px" value="@bind(vm.username)"/>
            </row>
            <row>
                <label value="Senha:"/>
                <textbox type="password" width="150px" value="@bind(vm.password)"/>
            </row>
            <row spans="2" align="center">
                <button label="Entrar" command="@command('doLogin')"/>
            </row>
        </rows>
    </grid>
</window>

LoginViewModel.java:

public class LoginViewModel {
    
    private String username;
    private String password;
    
    // Getters e setters obrigatórios para data binding
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    
    @Command
    public void doLogin() {
        // Usa diretamente as propriedades da classe
        // Sincronização automática com UI via binder
        
        // ... lógica de autenticação usando this.username e this.password ...
    }
}

Referências