A-ESIST

A-ESIST

Roles

  • React Native Developer
  • UI/UX Designer

Technologies Used

  • React Native
  • Expo
  • Figma
  • TypeScript

Overview

A-ESIST is a comprehensive mobile application developed for ZD Ljubljana, designed to guide healthcare professionals in implementing the ABCDE approach. This approach is a systematic method for assessing and treating critically ill patients. The app serves as an educational tool rather than real-time use, ensuring that healthcare staff can effectively learn and practice the ABCDE method.

Project Objectives

The primary goal of the A-ESIST app is to ensure healthcare professionals have a reliable and easy-to-use tool to support them in high-pressure situations, providing a step-by-step guide for patient assessment and management.

My Contributions

This project marked a significant milestone for me as it was my first professional experience working directly with a client. Below are the key areas where I contributed:

Client Communication

Engaging with the client was a new experience for me, where I focused on understanding their needs, creating a project outline, and accurately estimating costs. I made it a priority to keep the client informed at every stage, ensuring transparency and smooth decision-making. This involved regular updates and detailed explanations, particularly when presenting different logo designs, each accompanied by a narrative that helped the client make informed choices.

Conceptualization

Working closely with stakeholders, I was responsible for defining the project scope and objectives from the outset. This foundation ensured that the project was aligned with the client's expectations and that all requirements were clear and achievable.

Visual Identity and UI/UX Design

I was responsible for designing the app’s visual identity and user interface. The initial design got positive feedback, but I kept refining it, knowing that design is an iterative process. I often find that giving myself some time to step back and return with fresh eyes helps me see new possibilities for improvement. Below is an example of the evolution of design.

Logo design and name

For the logo design and naming process, I began by creating three initial variations, each with a focus on clean, modern colors that resonate with the medical theme. I always try to create multiple designs because I understand the importance of exploring various options. Often, I find myself gravitating toward a particular design, which might not necessarily be the best fit for the client or the project. By developing multiple variations, I ensure a broader exploration of possibilities and prevent myself from becoming too attached to a single idea. Below are the two out of three initial logo designs and narrative.

A-ESIST first logo variation with name

A-ESIST first logo variation with name

I avoid overly complex designs that could lose their impact when scaled down, especially when the logo is presented as an app icon on mobile devices. Instead, my focus is on crafting logos that are straightforward yet distinctive. To help the client make an informed decision, I sent them visual mockups showing how each icon variation would appear on an actual device. A-ESIST first logo variation with name

Design Process

I designed the A-ESIST app's user interface in Figma, going through multiple iterations to refine the look and feel. Each version of the design helped me see what worked well and what needed changes. Below are two images that show how the design evolved over time, highlighting the changes made to improve usability and visual appeal:

A-ESIST first logo variation with name

Development and Code Structure

For the development of this application, I chose to use React Native with Expo. Expo greatly simplified the process of development and testing, especially for the client, as it allowed for easy sharing of the app for testing purposes through QR codes or direct links, without the need for building APK or IPA files for Android and iOS.

To ensure a strong, type-safe codebase, I used TypeScript alongside React Native. TypeScript’s static type checking helped catch potential errors at compile time rather than runtime, significantly improving the robustness and maintainability of the code. Additionally, I utilized ESLint for linting, which enforced a consistent coding style and prevented common mistakes by providing real-time feedback in the code editor. I learned the importance of these tools during my time at the Povio academy, where they emphasized best practices in code quality and collaboration.

A significant technical challenge in this project was the need to create a dynamic rendering system capable of handling the branching logic of the ABCDE questionnaire. The questionnaire had a complex structure, with multiple paths depending on user input. To manage this complexity, I designed a system that stored the questionnaire data in JSON format. This allowed for flexible and efficient rendering of questions and answers based on user choices, enabling a truly dynamic user experience.

Questionnaire Component

The Questionnaire component is the central piece that manages the flow of questions based on user input. It uses React Navigation to handle screen transitions and Zustand for state management. Here’s how the Questionnaire component integrates dynamic rendering and handles user responses:

import { useNavigation } from "@react-navigation/native";
import { StackNavigationProp } from "@react-navigation/stack";
import { useEffect } from "react";
import DynamicRenderer from "./DynamicRenderer";
import { Question, Answer, RootStackParamList } from "../../types/types";
import { useAnswerStore, useNavigationStore } from "../../store";
import { SCREENS } from "../../constants/navigationConstants";
import { COMPONENT_TYPES } from "../../constants/componentTypes";

const END_OF_QUESTIONNAIRE = -1;
const NONE = "none";

function Questionnaire({
  questions,
  minutes,
  seconds,
  currentQuestionIndex,
  setCurrentQuestionIndex,
}: QuestionnaireProps) {
  const navigation = useNavigation<StackNavigationProp<RootStackParamList>>();
  const { addAnswer, setCurrentCategory } = useAnswerStore();
  const { pushToHistory } = useNavigationStore();

  useEffect(() => {
    const currentCategory = questions[currentQuestionIndex]?.category || NONE;
    setCurrentCategory(currentCategory);
  }, [currentQuestionIndex, questions, setCurrentCategory]);

  const getNextQuestionIndex = (answer: string) => {
    const currentQuestion = questions[currentQuestionIndex];
    const { nextId, edges, componentType, options } = currentQuestion;

    if (nextId === "end") {
      return END_OF_QUESTIONNAIRE; // Indicates the end of the questionnaire
    }

    let nextQuestionId = nextId;

    if (edges) {
      const selectedEdge = edges.find((edge) => edge.answer === answer);
      nextQuestionId = selectedEdge?.nextId || nextQuestionId;
    } else if (
      componentType === COMPONENT_TYPES.CUSTOM_BUTTONS_OPTION &&
      options
    ) {
      const selectedOption = options.find((option) => option.value === answer);
      nextQuestionId = selectedOption?.nextId || nextQuestionId;
    }

    if (nextQuestionId) {
      const nextIndex = questions.findIndex(
        (question) => question.id === nextQuestionId
      );
      return nextIndex >= 0 ? nextIndex : null;
    }

    return null;
  };

  const handleAnswer = (answer: string) => {
    const nextQuestionIndex = getNextQuestionIndex(answer);
    const { id, title, category } = questions[currentQuestionIndex];

    const newAnswer: Answer = {
      id,
      questionText: title,
      answer,
      questionIndex: currentQuestionIndex,
      category: category || NONE,
      seconds,
      minutes,
    };

    if (newAnswer.answer?.trim()) {
      addAnswer(newAnswer);
    }

    if (nextQuestionIndex === END_OF_QUESTIONNAIRE) {
      navigation.navigate(SCREENS.END_SCREEN);
    } else if (nextQuestionIndex !== null) {
      setCurrentQuestionIndex(nextQuestionIndex);
      pushToHistory(nextQuestionIndex);
    }
  };

  if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.length) {
    return null;
  }

  const currentQuestion = questions[currentQuestionIndex];

  return <DynamicRenderer question={currentQuestion} onAnswer={handleAnswer} />;
}

export default Questionnaire;

Dynamic Renderer

The DynamicRenderer component is responsible for rendering the specific question type (e.g., Yes/No, Multiple Choice, Input, Forms...) based on the configuration in the JSON file:

import YesNoOption from "./options/YesNoOption";
import Prompt from "../main/Prompt";
import { Question } from "../../types/types";
import ContinueOption from "./options/ContinueOption";
import BreathingForm from "./options/BreathingForm";
// ... other imports

interface DynamicRendererProps {
  question: Question;
  onAnswer: (answer?: string | string[]) => void;
}

function DynamicRenderer({ question, onAnswer }: DynamicRendererProps) {
  let content: React.ReactNode;

  const titleColor = getTitleColor(question.titleColor);

  switch (question.componentType) {
    case "YesNoOption":
      content = <YesNoOption onAnswer={onAnswer} />;
      break;
    case "BreathingForm":
      content = (
        <BreathingForm onNext={() => onAnswer()} showText={question.showText} />
      );
      break;
    // ... other cases
    default:
      content = <div>Invalid component type.</div>;
  }

  return (
    <Prompt title={question.title} titleColor={titleColor}>
      {content}
    </Prompt>
  );
}

export default DynamicRenderer;

State Management with Zustand

I used Zustand for state management to keep the application’s state logic clean and straightforward. Zustand provides a lightweight solution compared to Redux and suits the needs of managing form states and navigation history.

import { create } from "zustand";
import {
  Answer,
  CirculationFormState,
  VitallyEndangeredState,
  BreathingFormState,
} from "./types/types";

interface AnswerStore {
  answers: Answer[];
  circulationForm: CirculationFormState;
  vitallyEndangered: VitallyEndangeredState;
  breathingForm: BreathingFormState;
  addAnswer: (answer: Answer) => void;
  clearAnswers: () => void;
  // ... other state management functions
}

export const useAnswerStore = create<AnswerStore>((set) => ({
  answers: [],
  addAnswer: (answer) =>
    set((state) => ({ answers: [...state.answers, answer] })),
  clearAnswers: () => set({ answers: [] }),
  // ... other state management logic
}));

GitHub and CD/CI

To maintain high code quality and streamline the integration process, I set up a Continuous Integration/Continuous Deployment (CI/CD) pipeline using GitHub Actions. Each pull request triggered a series of automated checks, including code linting (with ESLint) and tests, to ensure that only clean, functional code was merged into the staging and main branches. This practice not only maintained code quality but also accelerated the development process by catching issues early.

Additionally, I created an automated system where, for each pull request, the app went through Expo, which generated a QR code for the release. This QR code was automatically sent to me via a Discord message, greatly simplifying the testing process by allowing me to instantly access the latest build directly on my mobile device. This streamlined workflow ensured rapid feedback cycles, making it easier to detect and fix issues early in the development process.

Through this project, I not only reinforced my skills in React Native, TypeScript, and Expo but also gained valuable experience in setting up a robust CI/CD pipeline and managing a clean, well-structured GitHub repository. The use of feature branching and automated checks provided a disciplined approach to collaborative development, ensuring a high-quality product.

I adopted a development philosophy focused on anticipating and avoiding future issues by writing clean, modular, and reusable code from the outset. I understand that sacrificing quality early on can lead to greater struggles later, as poorly structured code can become increasingly difficult to maintain, debug, and extend. By prioritizing best practices, clear code structure, and automation, I aimed to minimize technical debt and ensure the app could be easily scaled and maintained in the long run.

However, despite my efforts to maintain high standards, I did encounter some technical debt as the project progressed. There were moments when, to meet tight deadlines, I had to prioritize getting things to work over maintaining optimal code quality. This sometimes meant pushing code that worked but wasn't as clean, modular, or reusable as I would have liked. Over time, these decisions made parts of the codebase harder to manage and maintain.

Reflecting on this experience, I realize that implementing more code refactoring during the project would have been beneficial. By regularly refactoring code, I could have mitigated some of the technical debt and improved the overall quality of the codebase. Moving forward, I aim to incorporate more frequent refactoring sessions into my development process, balancing the need to meet deadlines with the importance of maintaining a clean and maintainable codebase.

JSON Structure for Dynamic Questions

The questionnaire's logic and flow are defined in a JSON file, making it easy to modify or extend without altering the core logic. Each question has properties such as id, title, componentType, and nextId to define its behavior and navigation path.

[
  {
    id: "PU1",
    title: "Daj pacienta v ležeči položaj",
    componentType: "ContinueOption",
    category: "PU",
    nextId: "PU2",
  },
  {
    id: "PU2",
    title: "Ali je v ustni votlini prisoten tujek?",
    componentType: "YesNoOption",
    category: "PU",
    edges: [
      {
        answer: "Da",
        nextId: "remove_foreign_body_pu",
      },
      {
        answer: "Ne",
        nextId: "clear_airway_options_pu",
      },
    ],
  },
  // ... other questions
];

Reflections on Building A-ESIST: Lessons in Testing, Refactoring, and Leveraging UI Libraries for Efficient Development

Building the A-ESIST application was a significant learning experience that revealed areas where I could optimize both the development process and the codebase. While the project successfully met its objectives, reflecting on it has highlighted some key takeaways that I would apply in future projects.

Refactoring for Cleaner Code

During the development, I found myself occasionally prioritizing speed over quality to meet deadlines, which led to the accumulation of some technical debt. I see the value of incorporating more regular refactoring sessions throughout the project lifecycle.

Leveraging Existing UI Libraries

Another lesson learned was the potential time savings that could have been achieved by using an already implemented UI library. I spent a considerable amount of time developing custom components, such as input fields, buttons, and handling platform-specific features like calendars for both iOS and Android. While this allowed for greater customization, it also slowed down the development process.

Using a well-established UI library could have provided robust, pre-built components that are both visually consistent and functional across platforms. This would have freed up time to focus on more complex, project-specific features rather than reinventing the wheel for basic UI elements.

Final Thoughts

Reflecting on these aspects, I see the importance of balancing custom development with smart use of existing tools. By writing cleaner, refactored code and leveraging mature UI libraries, future projects can benefit from a more streamlined development process, reducing technical debt and delivering a high-quality product faster. These insights will guide my approach in upcoming projects, helping me to build more scalable and maintainable applications.