Desenvolvimento de Cartões Dinâmicos em HarmonyOS: Guia Prático com Ligação de Dados

Visão Geral dos Cartões Serviço e Benefícios

Os cartões serviço no HarmonyOS são componentes UI leves com características principais como:

  • Exibição Avançada: Mostra informações cruciais diretamente na tela inicial ou bloqueio, sem necessidade de abrir o aplicativo.
  • Interatividade Simples: Suporte para ações básicas como toques, facilitando acesso rápido.
  • Compatibilidade Multi-dispositivo: Funciona em smartphones, tablets, PCs, smart TVs, relógios intleigentes (exceto dispositivos vestíveis ultra-leves).
  • Desenvolvimento Eficiente: Templates automáticos do IDE e SDK aceleram o desenvolvimento.

Configuração Básica do Ambiente

1. Declaração de Permissões

No modulo.json5, declare as permissões essenciais:

{
  "modulo": {
    "permissoesRequeridas": [
      { "nome": "ohos.permission.REQUERIR_FORMA" }
    ],
    "capacidades": [
      {
        "nome": "CapacidadeFormaEntrada",
        "tipo": "extensao",
        "tipoExtensaoCapacidade": "forma",
        "metadados": [
          { "nome": "ohos.extensao.forma", "recurso": "$perfil:config_forma" }
        ]
      }
    ]
  }
}

2. Arquivo de Configuração da Forma

Configure os atributos da forma no src/main/recursos/base/perfil/config_forma.json:

{
  "formas": [
    {
      "nome": "widget",
      "nomeExibicao": "$string:nome_exibicao_widget",
      "descricao": "$string:desc_widget",
      "fonte": "./ets/widget/paginas/WidgetCard.ets",
      "sintaxeUI": "arkts",
      "janela": { "larguraDesign": 720, "autoLarguraDesign": true },
      "modoCor": "auto",
      "dinamico": true,
      "padrao": true,
      "atualizacaoHabilitada": true,
      "horaAtualizacaoProgramada": "10:30",
      "duracaoAtualizacao": 1,
      "dimensaoPadrao": "2 * 4",
      "suportaDimensoes": ["2 * 2", "2 * 4", "4 * 4"]
    }
  ]
}

Implementação de Cartões Dinâmicos

1. Criação da Capacidade de Extensão da Forma

import { CapacidadeExtensaoForma, dadosVinculadosForma, provedorForma } from '@kit.FormKit';
import { Querer } from '@kit.CapacidadeKit';
import { ErroNegocio } from '@ohos.base';
import log from '@ohos.log';

const DOMINIO = 0x00001;
const ETIQUETA = 'CapacidadeFormaEntrada';

export default class CapacidadeFormaEntrada extends CapacidadeExtensaoForma {
  onAdicionarForma(querer: Querer): dadosVinculadosForma.DadosVinculadosForma {
    log.info(DOMINIO, ETIQUETA, 'onAdicionarForma chamado');
    
    const idForma = querer.parametros?.['ohos.extra.param.key.form_identity'] as string;
    const nomeForma = querer.parametros?.['ohos.extra.param.key.form_nome'] as string;
    
    const dadosIniciais = {
      titulo: 'Cartão Dinâmico',
      conteudo: 'Conteúdo Inicial',
      horaAtualizacao: new Date().toLocaleTimeString(),
      idForma: idForma
    };
    
    return dadosVinculadosForma.criarDadosVinculadosForma(dadosIniciais);
  }

  onEventoForma(idForma: string, mensagem: string): void {
    log.info(DOMINIO, ETIQUETA, `onEventoForma: idForma=${idForma}, mensagem=${mensagem}`);
    
    try {
      const parametros = JSON.parse(mensagem);
      this.manipularEventoForma(idForma, parametros);
    } catch (erro) {
      log.error(DOMINIO, ETIQUETA, `Falha ao analisar mensagem: ${(erro as ErroNegocio).mensagem}`);
    }
  }

  private async manipularEventoForma(idForma: string, parametros: any): Promise<void> {
    if (parametros.acao === 'atualizar') {
      await this.atualizarDadosForma(idForma);
    } else if (parametros.acao === 'alterarConteudo') {
      await this.alterarConteudoForma(idForma, parametros.conteudo);
    }
  }

  private async atualizarDadosForma(idForma: string): Promise<void> {
    const novosDados = {
      titulo: 'Cartão Dinâmico',
      conteudo: `Hora Atualização: ${new Date().toLocaleTimeString()}`,
      horaAtualizacao: new Date().toLocaleTimeString()
    };
    
    await this.atualizarDadosForma(idForma, novosDados);
  }

  private async alterarConteudoForma(idForma: string, conteudo: string): Promise<void> {
    const novosDados = {
      conteudo: conteudo,
      horaAtualizacao: new Date().toLocaleTimeString()
    };
    
    await this.atualizarDadosForma(idForma, novosDados);
  }

  private async atualizarDadosForma(idForma: string, dados: any): Promise<void> {
    try {
      const vinculoForma = dadosVinculadosForma.criarDadosVinculadosForma(dados);
      await provedorForma.atualizarForma(idForma, vinculoForma);
      log.info(DOMINIO, ETIQUETA, 'Dados da forma atualizados com sucesso');
    } catch (erro) {
      log.error(DOMINIO, ETIQUETA, `Falha ao atualizar forma: ${(erro as ErroNegocio).mensagem}`);
    }
  }

  onRemoverForma(idForma: string): void {
    log.info(DOMINIO, ETIQUETA, `Forma removida: ${idForma}`);
  }
}

2. Implementação do Componente UI Dinâmico

// WidgetCard.ets
import { enviarAcaoCartao } from '@kit.FormKit';

@PontoEntrada
@Componente
struct WidgetCard {
  @PropriedadeArmazenamentoLocal('titulo') titulo: string = 'Título Padrão';
  @PropriedadeArmazenamentoLocal('conteudo') conteudo: string = 'Conteúdo Padrão';
  @PropriedadeArmazenamentoLocal('horaAtualizacao') horaAtualizacao: string = '';
  @PropriedadeArmazenamentoLocal('idForma') idForma: string = '';

  build() {
    Coluna() {
      Text(this.titulo)
        .tamanhoFonte(18)
        .pesoFonte(FontPeso.Negrito)
        .margem({ topo: 10, baixo: 5 })
        .alinhamentoTexto(TextoAlinhamento.Centro)

      Text(this.conteudo)
        .tamanhoFonte(16)
        .margem({ baixo: 10 })
        .alinhamentoTexto(TextoAlinhamento.Centro)

      Text(`Atualizado: ${this.horaAtualizacao}`)
        .tamanhoFonte(12)
        .corFonte(Cor.Cinza)
        .margem({ baixo: 10 })

      Linha() {
        Botao('Atualizar')
          .onClick(() => this.enviarMensagem('atualizar'))
          .margem(5)

        Botao('Alterar')
          .onClick(() => this.enviarMensagem('alterarConteudo', { conteudo: 'Novo Conteúdo' }))
          .margem(5)
      }
      .margem({ topo: 10 })
    }
    .largura('100%')
    .altura('100%')
    .preenchimento(10)
    .corFundo(Cor.Branco)
  }

  private enviarMensagem(acao: string, parametros?: object): void {
    const mensagem = {
      acao: acao,
      idForma: this.idForma,
      ...parametros
    };

    enviarAcaoCartao(this, {
      acao: 'mensagem',
      parametros: JSON.stringify(mensagem)
    });
  }
}

Ligação de Dados e Gerenciamento de Estado

1. Ligação de Dados Local

// Criar instância de Armazenamento Local
const armazenamentoLocal = new ArmazenamentoLocal();

@PontoEntrada(armazenamentoLocal)
@Componente
struct CartaoPrincipalWidget {
  @PropriedadeArmazenamentoLocal('titulo') titulo: string = '';
  @PropriedadeArmazenamentoLocal('dados') @Observador('onDadosAlterado') dados: objeto = {};

  onDadosAlterado(): void {
    console.info('Dados atualizados:', this.dados);
  }

  build() {
    Coluna() {
      if (this.dados.itens && this.dados.itens.length > 0) {
        ForEach(this.dados.itens, (item: qualquer) => {
          Text(item.texto)
            .tamanhoFonte(14)
            .margem(2)
        })
      }

      Botao('Adicionar Item')
        .onClick(() => this.adicionarItem())
    }
  }

  private adicionarItem(): void {
    enviarAcaoCartao(this, {
      acao: 'mensagem',
      parametros: JSON.stringify({
        acao: 'adicionarItem',
        texto: `Item${Date.now()}`
      })
    });
  }
}

2. Mecanismo de Atualização de Dados em Tempo Real

// No CapacidadeExtensaoForma implementar atualizações periódicas
export default class CapacidadeDadosForma extends CapacidadeExtensaoForma {
  private temporizadores: Map<string, número> = new Map();

  onAdicionarForma(querer: Querer): dadosVinculadosForma.DadosVinculadosForma {
    const idForma = querer.parametros?.['ohos.extra.param.key.form_identity'] as string;
    
    this.iniciarAtualizacaoPeriodica(idForma);
    
    return dadosVinculadosForma.criarDadosVinculadosForma({
      itens: this.obterDadosIniciais()
    });
  }

  private iniciarAtualizacaoPeriodica(idForma: string): void {
    const temporizador = setInterval(async () => {
      await this.atualizarDadosForma(idForma);
    }, 30000);

    this.temporizadores.set(idForma, temporizador);
  }

  private async atualizarDadosForma(idForma: string): Promise<void> {
    const novosDados = {
      itens: this.buscarUltimosDados(),
      horaAtualizacao: new Date().toLocaleTimeString()
    };

    try {
      const vinculoForma = dadosVinculadosForma.criarDadosVinculadosForma(novosDados);
      await provedorForma.atualizarForma(idForma, vinculoForma);
    } catch (erro) {
      log.error(DOMINIO, ETIQUETA, `Falha na atualização periódica: ${(erro as ErroNegocio).mensagem}`);
    }
  }

  onRemoverForma(idForma: string): void {
    const temporizador = this.temporizadores.get(idForma);
    if (temporizador) {
      clearInterval(temporizador);
      this.temporizadores.delete(idForma);
    }
  }
}

Sincronização de Dados Distribuída

O HarmonyOS permite sincronização de dados entre dispositivos usando capacidades distribuídas.

import { dadosDistribuidos } from '@kit.DadosDistribuidos';

class GestorDadosDistribuidos {
  private gerenciadorKV: dadosDistribuidos.GerenciadorKV | null = null;
  private armazenamentoKV: dadosDistribuidos.ArmazenamentoUnicoKV | null = null;

  async inicializarArmazenamentoDistribuidoKV(): Promise<void> {
    try {
      const contexto = getContext(this) as Contexto;
      const configuracao: dadosDistribuidos.Configuracao = {
        nomePacote: contexto.informacoesAplicacao.nome,
        informacoesUsuario: {
          idUsuario: dadosDistribuidos.TipoUsuario.MESMO_ID_USUARIO
        }
      };

      this.gerenciadorKV = dadosDistribuidos.criarGerenciadorKV(configuracao);
      const opcoes: dadosDistribuidos.ConfiguracaoArmazenamento = {
        idArmazenamento: 'armazenamento_dados_widget',
        tipoArmazenamentoKV: dadosDistribuidos.TipoArmazenamentoKV.VERSAO_UNICA,
        nivelSeguranca: dadosDistribuidos.NivelSeguranca.S2,
        sincronizacaoAutomatica: true
      };

      this.armazenamentoKV = await this.gerenciadorKV.obterArmazenamento<dadosDistribuidos.ArmazenamentoUnicoKV>(opcoes);
    } catch (erro) {
      log.error(DOMINIO, ETIQUETA, `Falha na inicialização do banco de dados distribuído: ${(erro as ErroNegocio).mensagem}`);
    }
  }

  async sincronizarDadosCartao(idForma: string, dados: any): Promise<void> {
    if (!this.armazenamentoKV) return;

    try {
      await this.armazenamentoKV.colocar(`${idForma}_dados`, JSON.stringify(dados));
      log.info(DOMINIO, ETIQUETA, 'Dados do cartão sincronizados com o banco de dados distribuído');
    } catch (erro) {
      log.error(DOMINIO, ETIQUETA, `Falha na sincronização de dados: ${(erro as ErroNegocio).mensagem}`);
    }
  }

  async obterDadosSincronizados(idForma: string): Promise<qualquer> {
    if (!this.armazenamentoKV) return null;

    try {
      const valor = await this.armazenamentoKV.obter(`${idForma}_dados`);
      return valor ? JSON.parse(valor.toString()) : null;
    } catch (erro) {
      log.error(DOMINIO, ETIQUETA, `Falha ao obter dados sincronizados: ${(erro as ErroNegocio).mensagem}`);
      return null;
    }
  }
}

Funcionalidades Avançadas e Interações

1. Adaptação de Tamanhos de Cartões

@PontoEntrada
@Componente
struct CartaoAdaptativoWidget {
  @PropriedadeArmazenamentoLocal('dimensao') dimensao: string = '2 * 4';
  @PropriedadeArmazenamentoLocal('dados') dados: qualquer;

  build() {
    Coluna() {
      if (this.dimensao === '2 * 2') {
        this.buildLayoutPequeno();
      } else if (this.dimensao === '2 * 4') {
        this.buildLayoutMedio();
      } else if (this.dimensao === '4 * 4') {
        this.buildLayoutGrande();
      }
    }
  }

  @Construtor
  buildLayoutPequeno() {
    Text(this.dados.titulo)
      .tamanhoFonte(16)
      .margem(5);
    
    Text(this.dados.valor)
      .tamanhoFonte(20)
      .pesoFonte(FontPeso.Negrito);
  }

  @Construtor
  buildLayoutMedio() {
    Linha() {
      Imagem(this.dados.icone)
        .largura(40)
        .altura(40)
        .margem({ direita: 10 })

      Coluna() {
        Text(this.dados.titulo)
          .tamanhoFonte(16)
        Text(this.dados.valor)
          .tamanhoFonte(18)
          .pesoFonte(FontPeso.Negrito)
      }
    }
  }

  @Construtor
  buildLayoutGrande() {
    Coluna() {
      Text(this.dados.titulo)
        .tamanhoFonte(18)
        .pesoFonte(FontPeso.Negrito)

      Divisor()
        .margem(10)

      ForEach(this.dados.itens, (item: qualquer) => {
        Linha() {
          Text(item.rotulo)
            .tamanhoFonte(14)
            .pesoLayout(1)
          Text(item.valor)
            .tamanhoFonte(14)
            .corFonte(Cor.Azul)
        }
        .largura('100%')
        .margem({ baixo: 5 })
      })

      Divisor()
        .margem(10)

      Text(`Atualizado em: ${this.dados.horaAtualizacao}`)
        .tamanhoFonte(12)
        .corFonte(Cor.Cinza)
    }
  }
}

2. Animações e Efeitos Visuais

@Componente
struct CartaoAnimadoWidget {
  @Estado privado estaAtualizando: boolean = false;
  @Estado privado rotacao: numero = 0;

  build() {
    Coluna() {
      Linha() {
        Imagem($r('app.media.icone_atualizar'))
          .largura(20)
          .altura(20)
          .rotacionar({ angulo: this.rotacao })
          .onClick(() => this.iniciarAnimacaoAtualizacao())

        Text('Atualizar Dados')
          .tamanhoFonte(14)
          .margem({ esquerda: 5 })
      }
      .margem(10)
      .onClick(() => this.manipularAtualizacao())
    }
  }

  private iniciarAnimacaoAtualizacao(): void {
    this.estaAtualizando = true;
    this.rotacao = 0;
    
    animarPara({
      duracao: 1000,
      iteracoes: -1, // Loop infinito
      curva: Curva.Linear
    }, () => {
      this.rotacao = 360;
    });
  }

  private pararAnimacao(): void {
    this.estaAtualizando = false;
    this.rotacao = 0;
  }

  private async manipularAtualizacao(): Promise<void> {
    this.iniciarAnimacaoAtualizacao();
    
    try {
      await new Promise(resolve => setTimeout(resolve, 2000));
      this.pararAnimacao();
    } catch (erro) {
      this.pararAnimacao();
    }
  }
}

Práticas Recomendadas e Otimização de Desempanho

1. Sugestões de Otimização de Desempenho

  1. Controle de Volume de Dados: Limitar a quantidade de dados transmitidos por cartão, idealmente até 5KB.
  2. Frequência de Atualização: Definir intervalos razoáveis para evitar impacto no desempenho.
  3. Gestão de Memória: Liberar recursos e listeners não utilizados.
  4. Otimização de Imagens: Usar imagens adequadas em tamanho para reduzir consumo de memória.

2. Considerações de Desanvolvimento

  • Restrições de Cartões: Cartões ArkTS suportam apenas módulos marcados como compatíveis, não permitem pacotes compartilhados nem desenvolvimento em linguagem nativa (C++).
  • Tratamento de Eventos: Eventos de cartão e uso devem ser tratados separadamente, evitando conflito de gestos.
  • Limitações de Depuração: Não há suporte para depuração com breakpoints, Hot Reload ou setTimeOut/setInterval.

3. Tratamento de Erros e Robustez

class ManipuladorErros {
  static tratarErroCartao(erro: ErroNegocio, contexto: string): void {
    log.error(DOMINIO, ETIQUETA, `Erro no Cartão [${contexto}]: ${erro.mensagem}`);
    
    if (erro.codigo === 1001) { // Supondo que 1001 é erro de rede
      this.exibirErroRede();
    } else if (erro.codigo === 1002) { // Erro de formato de dados
      this.exibirErroDados();
    }
  }

  static exibirErroRede(): void {
    enviarAcaoCartao(this, {
      acao: 'mensagem',
      parametros: JSON.stringify({
        acao: 'exibirErro',
        mensagem: 'Falha na conexão de rede, verifique configurações'
      })
    });
  }

  static exibirErroDados(): void {
    enviarAcaoCartao(this, {
      acao: 'mensagem',
      parametros: JSON.stringify({
        acao: 'exibirErro',
        mensagem: 'Formato de dados incorreto, tente atualizar'
      })
    });
  }
}

Tags: HarmonyOS ArkTS ServiçosCartoes DesenvolvimentoMobile DadosDistribuidos

Publicado em 9-14 13:04