Da sessão 101 no Couchbase LIVE New York pista móvelNo artigo "Couchbase Lite", falamos sobre como começar a integrar o Couchbase Lite aos seus projetos iOS e Android. A partir do Slides "Couchbase Mobile 101: Como criar seu primeiro aplicativo móvel"No primeiro dia, exploramos as APIs do Couchbase Mobile, percorrendo o aplicativo de amostra Grocery Sync, que pode ser encontrado no repositório do Github para iOS e Android. Neste blog, vamos recapitular em alto nível os recursos e as APIs do Couchbase Lite que foram apresentados na sessão Couchbase 101, bem como alguns dos códigos encontrados na amostra Grocery Sync. Para começar, você deve baixar o Couchbase Lite Enterprise Edition para a plataforma em que você está desenvolvendo e siga o tutorial para iOS ou o tutorial para Android para integrar o Couchbase Lite aos seus projetos móveis.
Depois de trazer o Couchbase Lite para seus projetos móveis, precisaremos inicializar o Couchbase Lite e recuperar ou criar um banco de dados. Abaixo estão alguns conceitos e requisitos do Couchbase Mobile que precisamos.
[1] Gerente
O Gerente é a classe de nível superior a ser referenciada na criação de um namespace para bancos de dados. Criar um banco de dados é simplesmente fazer referência a um nome de cadeia de caracteres, como abaixo:
iOS
|
1 2 3 4 5 6 7 8 9 |
NSError* error; self.database = [[CBLManager sharedInstance] databaseNamed: kDatabaseName error: &error]; if (!self.database) { [self showAlert: @"Couldn't open database" error: error fatal: YES]; return NO; } |
Android
|
1 2 3 4 |
manager = new Manager(new AndroidContext(this), Manager.DEFAULT_OPTIONS); database = manager.getDatabase(DATABASE_NAME); |
Com esse código em vigor, podemos recuperar os documentos que contêm JSON do banco de dados adequadamente. O banco de dados também serve como fonte e destino para a replicação. Cada um dos documentos tem seu respectivo nome e ID exclusivos. Além disso, eles têm JSON como suas propriedades, sendo que o objeto JSON é um conjunto de propriedades de nome em que seus valores podem ser nomes ou cadeias de caracteres, números, matrizes, dicionários etc.
[2] Documentos
O Documento inclui um ID de documento imutável no banco de dados, no qual o corpo do documento assume a forma de um objeto JSON aninhado de pares de valores-chave. Para permitir a coexistência de diferentes tipos de documentos em um banco de dados, a convenção comumente usada é incluir uma propriedade chamada "type" (tipo), que tem uma cadeia de caracteres que define o tipo de seus documentos. Essa é uma técnica usada para manter o controle dos diferentes tipos de documentos se houver mais de um tipo em um banco de dados e também para ajudar na indexação. Os documentos também contêm revisões para fins de rastreamento de históricos de alterações e conflitos, portanto, é fundamental para o funcionamento da replicação.
Para inserir documentos, o botão 'createDocument()' retornará um ID de um documento que está na forma de um UUID gerado aleatoriamente. No aplicativo de amostra para iOS, um "NSDictionary" é criado para corresponder a um "NSObject" em Objective-C com propriedades de "text", "check" e "created_at" definidas. Abaixo para ...
iOS
|
1 2 3 4 5 6 7 8 9 10 11 12 |
NSDictionary *document = @{@"text": text, @"check": @NO, @"created_at": [CBLJSON JSONObjectWithDate: [NSDate date]]}; // Save the document: CBLDocument* doc = [database createDocument]; NSError* error; if (![doc putProperties: document error: &error]) { [self showErrorAlert: @"Couldn't save new item" forError: error]; } |
criamos as propriedades do novo documento e, em seguida, salvamos o documento fazendo referência à base de dados para o método 'createDocument()'. O "JSONObjectWithDate" é uma função utilitária que pega um objeto de data do Cocao e o converte em um formato de cadeia de caracteres ISO8601, pois as datas não podem ser armazenadas como objetos nativos no JSON.
Para o Android, a classe "SimpleDateFormat" está criando o "currentTimeString" para o objeto em que o ID do documento é construído pela combinação do "currentTime" da classe "Calendar" e o UUID do método "randomUUID()". Fazendo referência ao "banco de dados" que foi criado a partir da classe Manager, um documento é criado chamando o mesmo método "createDocument()" do iOS. No Android, os pares de chave-valor se assemelham a uma estrutura de objeto HashMap e, portanto, criamos um mapa que é o equivalente em Java do objeto JSON na variável "properties". As mesmas três propriedades são inseridas no mapa Java pelo método 'put()' e, em seguida, para persistir no disco, o objeto Map é passado para o método 'putProperties()'. Isso é ilustrado abaixo:
Android
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
SimpleDateFormat dateFormatter = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); UUID uuid = UUID.randomUUID(); Calendar calendar = GregorianCalendar.getInstance(); long currentTime = calendar.getTimeInMillis(); String currentTimeString = dateFormatter.format(calendar.getTime()); String id = currentTime + "-" + uuid.toString(); Document document = database.createDocument(); Map(String, Object) properties = new HashMap(String, Object)(); properties.put("_id", id); properties.put("text", text); properties.put("check", Boolean.FALSE); properties.put("created_at", currentTimeString); document.putProperties(properties); |
[3] Anexos
O Anexo O recurso JSON, embora não seja usado na amostra, permite que os documentos anexem qualquer blob binário de tamanho arbitrário e, portanto, é uma técnica de otimização para replicação em que as atualizações de documentos são independentes das atualizações de anexos, pois são armazenadas separadamente do corpo JSON. Por exemplo, esse pode ser um caso de uso para quando os metadados, o JSON do documento, são alterados em um anexo associado e, portanto, se um documento for atualizado sem alterações em um anexo, o replicador poderá ignorar o envio do anexo.
[4] Visualizações
O Ver permite que os aplicativos criem e mantenham índices secundários usando a técnica map & reduce. Começamos com um documento JSON e a função de mapa é a função que você escreve que recebe esse documento como entrada e gera um conjunto de pares de valores-chave. A saída dessa função de mapa que é executada em todos os documentos do banco de dados gera um índice. No aplicativo de amostra Grocery Sync, estamos definindo uma visualização com uma função de mapa que indexa os itens de tarefas por data de criação. Criando uma visualização para...
iOS
|
1 2 3 4 5 6 7 |
[[theDatabase viewNamed: @"byDate"] setMapBlock: MAPBLOCK({ id date = doc[@"created_at"]; if (date) emit(date, nil); }) version: @"1.1"]; |
Para o iOS, primeiro estamos criando uma "visualização" no banco de dados. O banco de dados também é um contêiner ou namespace para as 'visualizações', portanto, dizemos 'viewNamed: @"byDate"' onde, se a visualização ainda não existir, nós a criaremos e, se existir, nós a retornaremos. O restante do bloco de código é para definir seu bloco de mapas. Portanto, essa é uma visualização MapReduce, o que significa que ela tem uma função de mapa. Obtemos a data do documento observando a propriedade "created_at". E se o documento tiver uma, nós a emitimos como a chave. No aplicativo Grocery Sync, ele não está emitindo nada para o valor porque, na verdade, está voltando para pegar o próprio documento para obter o restante dos dados. A cadeia de caracteres de versão, '@"1.1″' no final, é usada para se comunicar com o banco de dados sobre se a função de mapa foi alterada ou não. Como o banco de dados não sabe dizer quando a função de mapa foi alterada de uma execução para outra, uma técnica é aumentar a string de versão para dizer ao banco de dados que jogue fora o índice atual e o reconstrua.
Android
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
com.couchbase.lite.View viewItemsByDate = database.getView(String.format("%s/%s", designDocName, byDateViewName)); viewItemsByDate.setMap(new Mapper() { @Override public void map(Map(String, Object) document, Emitter emitter) { Object createdAt = document.get("created_at"); if (createdAt != null) { emitter.emit(createdAt.toString(), null); } } }, "1.0"); |
Essa é a versão Java-Android em que, de forma semelhante ao iOS, chamamos 'database.getView()' e, em seguida, criamos a 'função map' usando alguma sintaxe de classe interna que, em Java, existe como um objeto 'Mapper()'. Os índices podem ser atualizados sob demanda e informações úteis podem ser extraídas do documento que você deseja indexar, onde o valor-chave pode ser emitido. Toda vez que algo muda, a função map é alimentada com esse documento. A função map chama uma função chamada 'emit()' que recebe a 'chave e o valor' como parâmetros.
O que o aplicativo Grocery Sync está fazendo aqui é gerar o índice de todos os itens de tarefas que são abreviados por "chave" e, como a chave é o carimbo de data/hora, os itens serão ordenados cronologicamente, onde as cadeias de valores são os nomes dos itens de tarefas. O índice também lembra o ID do documento que emitiu esse par chave-valor. Assim, quando você estiver consultando e tiver uma linha na consulta, poderá usá-la para voltar ao documento e buscar essa linha inteira no banco de dados, se desejar. A ideia aqui é que, uma vez que você tenha esse índice, você o consulte. A mentalidade de consulta é feita dizendo: "Quero todas as entradas de índice com uma chave específica, um conjunto de chaves ou um intervalo de chaves".
[5] Consultas
As consultas podem, então, procurar um intervalo de linhas em uma visualização e usar as chaves e os valores das linhas diretamente ou obter os documentos de onde vieram a partir do ID do documento. No código abaixo, estamos conduzindo a tabela a partir de uma consulta de visualização, criando uma consulta classificada por data decrescente em que os itens mais recentes são exibidos primeiro.
iOS
|
1 2 3 4 5 6 7 8 9 |
CBLLiveQuery* query = [[[database viewNamed:@"byDate"] query] asLiveQuery]; query.descending = YES; // Plug the query into the CBLUITableSource, which uses it to drive the table. // (The CBLUITableSource uses KVO to observe the query's .rows property.) self.dataSource.query = query; self.dataSource.labelProperty = @"text"; |
Com os itens no índice, queremos usar esse índice para conduzir a exibição de tabela, que é a principal interface do usuário do Grocery Sync. No iOS, geramos uma consulta que é 'viewNamed: @"byDate"' e chamamos a consulta que cria uma consulta sobre ela. Em seguida, vemos "LiveQuery", que é um subconjunto especial de consulta que realmente rastreará a visualização ao longo do tempo. Definimos a propriedade "descending" (descendente) na consulta como "yes" (sim), pois queremos obter as linhas em ordem descendente das datas para que os itens criados mais recentemente fiquem no topo. Por fim, com o código específico do iOS, estamos informando ao 'dataSource' sobre a consulta e indicando qual propriedade deve ser exibida como rótulo na exibição de tabela.
Android
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
liveQuery = view.createQuery().toLiveQuery(); liveQuery.addChangeListener(new LiveQuery.ChangeListener() { public void changed(final LiveQuery.ChangeEvent event) { runOnUiThread(new Runnable() { public void run() { grocerySyncArrayAdapter.clear(); for (Iterator(QueryRow) it = event.getRows(); it.hasNext();) { grocerySyncArrayAdapter.add(it.next()); } grocerySyncArrayAdapter.notifyDataSetChanged(); |
Da mesma forma, a versão Java-Android é iniciada da mesma forma, com a criação de uma consulta por meio da chamada da visualização "toLiveQuery()" para gerar a "liveQuery". E, em seguida, "addChangeListener()" é executado nessa liveQuery, seguido pelo método "changed()" sendo chamado para as atualizações da consulta, que é sempre que o resultado dessa consulta é alterado. E a saída para quando você executa a consulta é uma matriz de "QueryRows", em que cada QueryRow é um objeto e tem propriedades como Key e Value e DocumentID, mas também uma propriedade de documento que carregará o documento de volta do banco de dados. Ele está passando por um "Iterator()" para obter todas as linhas da consulta e adicioná-las ao "grocerySyncArrayAdapter", que é uma classe personalizada que ele tem para armazenar o conjunto de dados.
[6] LiveQuery
Podemos pensar no LiveQuery como um invólucro em torno da consulta que escuta as notificações de alteração do banco de dados. Assim, quando o banco de dados for alterado, o LiveQuery iniciará a consulta novamente, executando-a em segundo plano de forma assíncrona. E, em seguida, compara os resultados da consulta com os resultados anteriores que já tinha. Se os resultados tiverem sido alterados, o LiveQuery sinalizará seus próprios eventos de notificação, que o aplicativo poderá tratar adequadamente, como redesenhar a interface do usuário com base nessa nova consulta. Abaixo, o código ilustra como exibir as células da tabela para...
iOS
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
- (void)couchTableSource:(CBLUITableSource*)source willUseCell:(UITableViewCell*)cell forRow:(CBLQueryRow*)row { // Set the cell background and font: ……… // Configure the cell contents. Map function (above) copies the doc properties // into its value, so we can read them without having to load the document. NSDictionary* rowValue = row.value; BOOL checked = [rowValue[@"check"] boolValue]; if (checked) { cell.textLabel.textColor = [UIColor grayColor]; cell.imageView.image = [UIImage imageNamed:@"checked"]; } else { cell.textLabel.textColor = [UIColor blackColor]; cell.imageView.image = [UIImage imageNamed: @"unchecked"]; } // cell.textLabel.text is already set, thanks to setting up labelProperty |
Para o aplicativo de amostra aqui, o "CBLUITableSource" do Couchbase Lite atuará como intermediário entre o LiveQuery e o UITableView, recebendo notificações de alteração e, assim, conduzindo a tabela com base em uma consulta. Ele também atua como o objeto de fonte de dados para o tableView, o que significa que é o objeto que o tableView solicitará para fornecer todos os dados das linhas. Seu objeto "controller" torna-se então o delegado do UITableView, onde receberá notificações do TableView sobre quando o usuário tocar em uma das linhas.
Android
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public View getView(int position, View itemView, ViewGroup parent) { //... TextView label = ((ViewHolder)itemView.getTag()).label; QueryRow row = getItem(position); SavedRevision currentRevision = row.getDocument().getCurrentRevision(); // Check box Object check = (Object) currentRevision.getProperty("check"); boolean isGroceryItemChecked = false; if (check != null && check instanceof Boolean) isGroceryItemChecked = ((Boolean)check).booleanValue(); // Text String groceryItemText = (String) currentRevision.getProperty("text"); label.setText(groceryItemText); |
Aqui está o equivalente para Android na classe Grocery Sync Adapter. Ele está obtendo o "QueryRow" chamando "getItem()" com base no número da linha na tabela. Em seguida, ele obtém o documento da linha de consulta e obtém sua revisão atual. Em seguida, ele usa as propriedades de verificação e texto para preencher os controles de IU na linha.
Por fim, no caso do aplicativo de amostra Grocery Sync, precisamos responder a um toque em uma linha da tabela, como, por exemplo, alternar a marca de seleção. Abaixo, ilustramos como isso é feito fazendo referência ao QueryRow em um índice específico e recuperando o documento a partir dele.
iOS
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { // Ask CBLUITableSource for the corresponding query row, and get its document: CBLQueryRow *row = [self.dataSource rowAtIndex:indexPath.row]; CBLDocument *doc = row.document; // Toggle the document's 'checked' property: NSMutableDictionary *docContent = [doc.properties mutableCopy]; BOOL wasChecked = [docContent[@"check"] boolValue]; docContent[@"check"] = @(!wasChecked); // Save changes: NSError* error; if (![doc.currentRevision putProperties: docContent error: &error]) { [self showErrorAlert: @"Failed to update item" forError: error]; } } |
Essa é uma chamada de método para o próprio UITableView em seu delegado. Dizendo que uma linha foi selecionada, o que significa "TAPPED", ele vai para a fonte de dados. Que é o objeto UI Table Source, e solicitará a Query Row nesse índice e obterá o documento a partir dele. Portanto, agora ele está basicamente fazendo um ciclo de leitura, gravação e modificação nesse documento. Ele obtém as propriedades, faz uma cópia mutável das propriedades e agora temos um dicionário mutável que pode ser atualizado. Lê a propriedade verificada como um booleano e a grava de volta como o oposto. Portanto, isso está invertendo o valor booleano da propriedade verificada. Em seguida, ele chama putProperties no final para salvar esse valor de volta.
Android
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public void onItemClick(AdapterView(?) adapterView, View view, int position, long id) { QueryRow row = (QueryRow) adapterView.getItemAtPosition(position); Document document = row.getDocument(); Map(String, Object) newProperties = new HashMap(String, Object)(document.getProperties()); boolean checked = ((Boolean) newProperties.get("check")).booleanValue(); newProperties.put("check", !checked); try { document.putProperties(newProperties); grocerySyncArrayAdapter.notifyDataSetChanged(); } catch (Exception e) { |
Passando agora para a versão Android, temos um 'onItemClick()' que é chamado pela GUI do Android. Ela obterá seu QueryRow nessa posição, obterá o documento, extrairá as propriedades e, em seguida, colocará as propriedades. Nas APIs Java, é idiomático usar exceções, o que não acontece no Objective C. Portanto, há uma tentativa de captura em torno da alça para salvar o documento. Se, na janela de tempo, outra coisa modificasse o documento, provavelmente o replicador, isso geraria um erro. Você receberia um erro de conflito.
Em seguida, veremos a classe Replicator do Couchbase Lite e a classe Portal do desenvolvedor do Couchbase Mobile é um ótimo recurso para começar.
A partir daí, vamos nos aprofundar em Gateway de sincronização do Couchbase em nossa sessão 102, onde falarei sobre "Como adicionar o Secure Sync ao seu aplicativo móvel". Concluiremos o dia mostrando como ativar o recurso Peer-to-Peer do Couchbase Mobile na sessão 103 com Austin Gonyouonde você pode criar experiências sociais exclusivas no aplicativo com "Building a Peer-to-Peer App with Couchbase Mobile".

