Executando verificação de segurança...
6

Criando um app de lista de tarefas em React Native

Olá, turma! Hoje vamos construir um app massinha em React Native. Ele é bem simples mesmo e é para você que quer começar nessa nova linguagem. Nele vamos aprender sobre componentização, mudança de estados e tudo o mais.

O nosso projeto se chamará todoList. Nada mais é que uma lista de tarefas. Nele poderemos adicionar uma tarefa e marcá-la como concluída.

Esse projeto é um desafio que tinha lá no curso da Rocketseat e eu me propus a fazer. E para deixar o desafio mais interessante, resolvi documentar tudo e criar esse tutorial para consolidar os meus conhecimentos. Bom, bora codar.

Iniciando o projeto

Para começar, no terminal e na pasta em que desejo criar o projeto, utilizo o comando:

expo init todolist --npm

Eu escolhi começar com a opção ‘blank (TypeScript)’.
Depois de instalado todas as dependências, podemos iniciar o projeto no vsCode (que é a ferramenta que eu uso, mas você pode usar outra de sua preferência).

code todolist

Vamos começar então removendo todo o conteúdo da função App e estruturando as nossas pastas. Eu gosto de deixar tudo de novo na pasta ‘src’. Src vem de ‘Source’, fonte em inglês. Nela então vai ficar todo o nosso código FONTE. Faz sentido, não faz?

Dentro da pasta src, criamos outra pasta chamada ‘screens’, nessa pasta ficarão as nossas telas. Aproveitando, vamos criar a nossa tela principal, e para isso eu gosto de organizar também em pastas. Criamos então uma pasta chamada Home e dentro dela colocamos dois arquivos: index.tsx que será o nosso componente de fato e o styles.ts que é onde ficará a estilização do nosso componente

Criando nosso componente Input

Bora então desenvolver o nosso app, começando a pensar nos componentes que podemos criar.

Captura de Tela 2023-07-19 às 08.55.36.png

Vamos começar pelo Input. Eu criei uma nova pasta dentro de ‘src’ chamada ‘components’ para colocar todos os componentes da aplicação. Nela, criei a pasta AddNewTodo, com o arquivo index.tsx e styles.ts para manter o padrão.

O componente final ficou assim:

import { TextInput, TouchableOpacity, View, Text } from "react-native";
import { styles } from "./styles";

export function AddNewTodo(){
  return (
    <View style={styles.container}>
      <TextInput
        style={styles.inputField}
        placeholder="Adicione uma nova tarefa"
        placeholderTextColor="#808080"
      />
      <TouchableOpacity 
        style={styles.button
        }>
        <View style={styles.iconButton}>
          <Text style={styles.textButton}>+</Text>
        </View>
      </TouchableOpacity>
    </View>
  )
}

Eu envolvi tanto o Input quanto o Botão (TouchableOpacity) em um container (View). Dentro do botão eu poderia simplesmente adicionar um ícone, mas resolvi usar os recursos do próprio react e criei outro container e um texto dentro dele.

A estilização ficou assim:

import { StyleSheet } from "react-native";

export const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    marginHorizontal: 24
  },
  inputField: {
    flex: 1,
    height: 54,
    padding: 16,
    backgroundColor: '#262626',
    borderRadius: 6,
    marginRight: 4,
    borderStyle: 'solid',
    borderWidth: 1,
    borderColor: '#0D0D0D',
    color: '#F2F2F2',
    fontSize: 16,
  },
  button: {
    width: 52,
    borderRadius: 6,
    backgroundColor: '#1E6F9F',
    justifyContent: 'center',
    alignItems: 'center',
  },
  iconButton: {
    width: 18,
    height: 18,
    borderColor: '#F2F2F2',
    borderStyle: 'solid',
    borderWidth: 1.5,
    borderRadius: 50,
    justifyContent: 'center',
    alignItems: 'center',
    alignContent: 'center'
  },
  textButton: {
    lineHeight: 16,
    color: '#F2F2F2',
  }
})

Agora, na nossa home, vamos criar o nosso cabeçalho, mas sem a imagem por enquanto:

export function Home(){
  return <View style={styles.header}/>
}

A estilização ficou dessa forma:

export const styles = StyleSheet.create({
  header: {
    width: '100%',
    height: 173,
    backgroundColor: '#0D0D0D'
  }
})

Vamos aproveitar e colocar também aqui dentro a estilização do nosso corpo. Basta adicionar o código:

body: {
    flex: 1,
    backgroundColor: '#1A1A1A'
  }

O flex 1 vai fazer que o corpo ocupe todo o espaço disponível que sobrou. Agora, na nossa Home, podemos adicionar o componente que criamos.

export function Home(){
  return (
    <>
      <View style={styles.header}/>
      <View style={styles.body}>
        <AddNewTodo/>
      </View>
    </>
  )
}

Note que adicionamos uma tag vazia ‘<>’ para embrulhar o nosso código. Isso é o Fragment do React que não tem nenhuma estilização e serve apenas envolver os elementos filhos para não dar erro na nossa aplicação. Para renderizar o componente corretamente, é preciso retornar sempre um único elemento pai.

O AddNewTodo foi adicionado dentro do nosso corpo, para que seja possível aparecer por cima do nosso cabeçalho. Fazemos isso modificando a estilização:

container: {
    position: 'absolute',
    top: -30,
    flexDirection: 'row',
    marginHorizontal: 24
  },

Para finalizar, eu adicinei a StatusBar com o tema light, já que o fundo da nossa aplicação é escuro. A propriedade translucent faz com que a barra de status permita que o conteúdo do aplicativo seja exibido abaixo dela.

export function Home(){
  return (
    <>
      <StatusBar barStyle={"light-content"}
        backgroundColor='transparent'
        translucent
      />
      <View style={styles.header}/>
      <View style={styles.body}>
        <AddNewTodo/>
      </View>
    </>
  )
}

E eis o resultado:

Captura de Tela 2023-07-19 às 10.14.10.png

Importando imagens

Eu coloquei 3 opções de tamanhos para que ele se adapte a diferente tamanhos de telas:

Captura de Tela 2023-07-19 às 11.50.39.png

Olha só como fica o nosso cabeçalho:

<View style={styles.header}>
    <Image source={require('../../assets/logo.png')}></Image>
</View>

Só centralizar tudo o que tem no header e o logo fica centralizado, bonitinho:

header: {
    justifyContent: 'center',
    alignItems: 'center'
  },

Criando o Componente Counter

Como os contadores de tarefas concluídas e criadas são muito semelhantes, podemos criar um único componente e reaproveitar código.

A solução que eu cheguei foi a seguinte:

  1. Já que estamos utilizando typescript, criei uma interface com as propriedades ‘name’ e ‘value’, já que são as duas únicas coisas que mudam no nosso componente.
interface CounterProps {
  name: 'Criadas' | 'Concluídas'
  value: number
}
  1. Criei o componente, fazendo com que, caso seja escolhido a opção ‘Criadas’ ele tenha uma determinada cor, caso contrário ele receberá outra cor.
export function Counter({name, value}: CounterProps){
  return (
    <View style={styles.container} >
      <Text style={
          [
            name === 'Criadas' ? styles.color1 : styles.color2,
            styles.text
          ]
        }>{name}</Text>
      <View style={styles.numberContainer}>
        <Text style={styles.number}>{value}</Text>
      </View>
    </View>
  )
}

Obs.: Para manter aqui abaixo dos 20000 caractares, vou ocultar os estilos do StyledSheed, mas você pode conferir tudo no link do github que deixarei abaixo.

Na nossa Home, colocamos os componentes abaixo do nosso Input, envolvido por uma View. Por enquanto com os valores aleatório, mas em breve implementaremos essa funcionalidade. Vamos terminar a estilização toda primeiro.

<View style={styles.bodyContent}>
   <Counter name="Criadas" value={1}/>
   <Counter name="Concluídas" value={4}/>
</View>

Captura de Tela 2023-07-19 às 13.48.25.png

Criando o componente Todo

Agora vamos criar o componente de tarefa. Nele precisamos ter um CheckBox. Eu aindei pesquisando e vi que não tem mais essa opção nativa no React Native, então tive que instalar uma biblioteca da comunidade:

npm i react-native-bouncy-checkbox

Instalação concluída, só importar o BouncyCheckbox e usar no nosso componente Todo. O legal é que ele já vem com uma animação bem maneirinha. Só tivemos que definir o tamanho e a cor das bordas. Mais para frente vamos voltar nele para fazer uns ajustes. Além disso, usamos o TouchableOpacity, dentro dele adicionando o ícone de lixo - e eis o nosso botão para excluir uma tarefa.

import { View, Text, TouchableOpacity, Image } from "react-native"
import BouncyCheckbox from "react-native-bouncy-checkbox";
import { styles } from "./style";

interface TodoProps {
  name: string
}

export function Todo({name} : TodoProps){
  return (
    <View style={styles.container}>
      <BouncyCheckbox
        size={24}
        fillColor="#5E60CE"
        innerIconStyle={styles.iconStyle}
      />
      <Text style={styles.text}>{name}</Text>
      <TouchableOpacity>
        <Image source={require('@assets/trash.png')}/>
      </TouchableOpacity>
    </View>
  )
}

Agora basta usarmos na nossa aplicação. E para isso eu vou utilizar o FlatList, que é um componente do react-native para lidar com listas. E legal desse componente é que ele renderiza os itens aos poucos na tela, confrome vai surgindo, e não tudo de uma vez. Isso faz com que fique bem performático.

const tasks = ['Integer urna interdum massa libero auctor neque turpis turpis semper.', 'lavar louça']
// Criei uma lista aqui para testarmos por enquanto

<FlatList 
    data={tasks} 
    renderItem={(todo) => 
      (
        <Todo name={todo.item} />
      )
}/>

Olha como está ficando! Show de bola, né? Agora só precisamos fazer tudo funcionar! 😀

Captura de Tela 2023-07-19 às 15.33.32.png

Implementando as funcionalidades

Antes de começar, vamos fazer alguns ajustes, para concentrar todas as alterações de estados na nossa home para ficar mais fácil. Em nossa interface do Todo, vamos adicionar a propriedade ‘onRemove’ e a propriedade ‘onChecked’. Pois queremos atualizar a tela quando realizamos essas ações.

interface TodoProps {
  name: string
  onChecked: (cheked: boolean) => void
  onRemove: () => void
}

Agora, podemos ir lá na nossa Home e colocar essas propriedades no nosso FlatList.

<FlatList 
  data={tasks} 
  renderItem={(todo) => 
    (
      <Todo 
      name={todo.item}
      onRemove={handleTodoRemove}
      onChecked={handleTodoCheck} />
    )
}/>

Vamos aproveitar e criar propriedades para o nosso AddNewTodoProps também:

interface AddNewTodoProps {
  onChange: (text:string) => void
  onPress: () => void
  value: string
}

export function AddNewTodo({onPress, onChange, value} : AddNewTodoProps){
 
  return (
    <View style={styles.container}>
      <TextInput
        style={styles.inputField}
        placeholder="Adicione uma nova tarefa"
        placeholderTextColor="#808080"
        onChangeText={onChange}
				value={value}
      />
      <TouchableOpacity 
        onPress={onPress}
        style={styles.button}>
        <View style={styles.iconButton}>
          <Text style={styles.textButton}>+</Text>
        </View>
      </TouchableOpacity>
    </View>
  )
}

Agora também podemos resgatar o texto na nossa home e lidar com o clique no botão utilizando o useState do React.

Na home eu criei mais uma interface para a Task, para sabermos quando uma tarefa está concluída ou não:

export interface TaskInterface {
  name: string,
  checked: boolean,
}

Então teremos alguns estados que vão mudar e precisamos usar o useState para refletir essas mudanças na tela:

  • Temos as Tasks, que será uma lista de TaskInterface
  • Temos a newTask que será uma string, serve para armazenarmos o valor da nova tarefa que vamos adicionar
  • Temos por fim o checkedTasks que verifica na nossa lista de tarefas quais que estão concluídas e retorna um número que a gente vai usar no nosso componente Counter.
const [tasks, setTasks] = useState<TaskInterface[]>([])
const [newTask, setNewTask] = useState('')
let checkedTasks = tasks.filter(tasks => tasks.checked === true).length

Inclusive já podemos modificar os nossos Counters:

<Counter name="Criadas" value={tasks.length}/>
<Counter name="Concluídas" value={checkedTasks}/>

Adicionando nova tarefa

Para adicionar uma nova tarefa, podemos fazer o seguinte:

<AddNewTodo onPress={handleTodoAdd} onChange={setNewTask} value={newTask}/>

Na função handleTodoAdd , procuramos na lista se há alguma tarefa com o mesmo nome, e caso positivo disparamos um alerta dizendo que a tarefa já existe. Isso vai evitar alguns erros.

Caso Não exista essa tarefa, adicionamos essa tarefa na lista com o setTasks e depois limpamos o input com o setNewTask vazio.

function handleTodoAdd(){
    const taskWithSameName = tasks.find((task) => task.name === newTask)
    if (taskWithSameName) {
      Alert.alert('Tarefa já existe', 'Já existe uma tarefa dessa criada')
      return
    }
    setTasks(prevState => [...prevState, {name: newTask, checked: false}])
    setNewTask('')
}

Removendo tarefa

Para remover a tarefa, nossa propriedade onRemove no componente Todo fica assim:

onRemove={() => handleTodoRemove(todo.item.name)}

Essa função, utilizamos também o setTasks, pegando os itens anteriores (prevState) e fazendo uma filtragem, excluindo o nome que é igual ao que temos.

function handleTodoRemove(name: string){
    setTasks(prevState => prevState.filter((item)=> item.name !== name))
}

Tarefa concluída

Para evitar erros, também adicionamos a propriedade key na lista. Como não teremos tarefas repetidas, podemos usar o próprio nome no campo.

Na propriedade onChecked, enviamos o objeto TaskInterface.

<Todo 
    key={todo.item.name}
    name={todo.item.name}
    onRemove={() => handleTodoRemove(todo.item.name)}
    onChecked={(isChecked) => {
      handleTodoCheck({
        name: todo.item.name,
        checked: isChecked
      })
}}/>

O handleTodoCheck é um map percorrendo a lista e verificando se o nome é igual para fazer a substituição dessa task pela nossa modificada.

function handleTodoCheck(task: TaskInterface){
  setTasks(prevState => prevState.map((item) => {
    if (item.name === task.name){
      return task
    } else {
      return item
    }
  }))
}

Agora, dentro do nosso componente Todo, atualizamos para que ele se comporte de maneira diferente, dependendo do seu estado (se a tarefa foi concluída ou não):

export function Todo({name, onRemove, onChecked} : TodoProps){
  const [isChecked, setIsChecked] = useState(false)

  return (
    <View style={[
      styles.container,
      isChecked && styles.finished
      ]}>
      <BouncyCheckbox
        size={24}
        fillColor="#5E60CE"
        innerIconStyle={isChecked ? styles.iconStyle2 : styles.iconStyle}
        onPress={(isChecked) => {
          onChecked(isChecked)
          setIsChecked(isChecked)
        }}
      />
      <Text style={isChecked ? styles.text2 : styles.text}>{name}</Text>
      <TouchableOpacity onPress={onRemove}>
        <Image source={require('@assets/trash.png')}/>
      </TouchableOpacity>
    </View>
  )
}

Caso a tarefa esteja concluída, ele vai adicionar o styles.finished, que nada mais é do que uma opacidade no container:

finished: {
    opacity: 0.6
},

Também, se estiver concluída, ele adiciona o styles.text2, que acrescenta aquele risquinho no texto, o tal do ‘line-through’:

text2: {
    width: '80%',
    color: '#F2F2F2',
    textDecorationLine: 'line-through'
},

E pronto, está quase finalizado. Só precisamos ajustar o nosso Input, pois queremos que a borda seja azul quando estiver o foco nele. Então vamos lá.

Mudando o estado do componente quando muda o foco

Esse aqui eu deixei para o final, pois foi um pouco difícil de achar a resposta, mas no fim acabei encontrando. Eu estava me perguntando como mudar a borda quando o TextInput está focado e quando mudar de novo a sua cor quando sair do foco. A solução que cheguei foi a seguinte: são duas propriedades que vamos trabalhar, a onFocus e a onBlur.

O onFocus dispara sempre que estiver focado enquanto que o onBlur dispara sempre que sair do foco. Então, no nosso AddNewTodo, adicionamos mais um hook para saber se está no foco ou não, começando naturalmente com false no seu estado inicial:

const [isFocus, setIsFocus] = useState(false)
return (
    <View style={styles.container}>
      <TextInput
        style={[styles.inputField,
        isFocus && styles.focusOutline
        ]}
        placeholder="Adicione uma nova tarefa"
        placeholderTextColor="#808080"
        onChangeText={onChange}
        value={value}
        onFocus={() => setIsFocus(true)}
        onBlur={()=> setIsFocus(false)}
      />
      <TouchableOpacity 
        onPress={onPress}
        style={styles.button}>
        <View style={styles.iconButton}>
          <Text style={styles.textButton}>+</Text>
        </View>
      </TouchableOpacity>
    </View>
  )

Caso esteja focado, o estilo focusOutiline será setado:

focusOutline: {
    borderColor: '#5E60CE',
 },

E finalizamoooos!!!! Nosso app já pode ser usado!!

Captura de Tela 2023-07-19 às 15.33.32.png

Obrigado por acompanhar até aqui! Até a próxima!

Carregando publicação patrocinada...
2

Oi, Felipe Ferreira!

Primeiramente parabéns pelo artigo, amei.

Eu posso te dar algumas dicas para deixar o artigo ainda melhor?

Na parte iniciando o projeto quando você já tem uma estrutura de pastas definita você pode antecipar o processo colocando-a no começo do artigo e colocando um print do comando tree para o leitor ver a estrutura.

Muito massa você já ter colocado o resultado final na tela, perfeito.

Sempre adicione uma imagem/texto de como ficou a classe depois do passo, por exemplo, nesse trecho “Vamos começar então removendo todo o conteúdo da função App [..]” Você poderia colocar como ficaria a classe App depois desse processo para evitar que o leitor apague alguma coisa a mais sem querer.

Na parte sobre AddNewTodo, sempre que um código pode apresentar um erro momentâneo, como por exemplo, o arquivo style.tsx ainda não está pronto, é sempre bom avisar para o leitor não pensar que ele fez algo errado.

A pasta assets você poderia colocar no google drive para o leitor já baixar e usar.

Atenção de sempre deixar bem claro onde você está fazendo alguma modificação, por exemplo, na parte de imagem esse trecho ficou um pouco confuso “Olha só como fica o nosso cabeçalho:”. O leitor pode ficar em dúvida de onde inserir o código e novamente a falta da visão da classe depois desse passo acaba atrapalhando um pouco.

Acho que é só isso. Os demais passo estão muito bem escritos e bem detalhados, mais uma vez parabéns pelo artigo/tutorial.

Você pode usar esses artigos para te ajudar a dar aquele up no readme do projeto

https://www.tabnews.com.br/natanael755/tutorial-criando-um-readme-bacana-para-seus-repositorios

https://www.tabnews.com.br/gabrielfpereira/a-forma-mais-facil-de-criar-um-readme

https://www.tabnews.com.br/HenriikOliveira/gerador-de-readme-para-github

Desculpe qualquer coisa e te desejo muito sucesso.

2

Excelente essa postagem que é um tutorial bem detalhado, gostei demais. Só faltou taklvez como exportar para Android ou IOS.
Que você coloque posts assim criando algo e ensinando desde os primórdios para ajudar programadores iniciantes.

1

Fala, man! Tudo bem? Desculpe responder só agora, mas só vi agora sua resposta hehe
Queria agradecer demais pelas dicas! Estou iniciando agora nesse mundo de tutoriais e documentação e suas dicas foram valiozas demais, muito obrigado mesmo!

Vou dar uma olhada com calma em tudo e logo colocar em prática! Abraços!