ホームページ  >  記事  >  ウェブフロントエンド  >  React Native を使用したオフラインファースト アプリケーションの構築

React Native を使用したオフラインファースト アプリケーションの構築

DDD
DDDオリジナル
2024-09-19 06:32:02467ブラウズ

Building Offline-first Applications with React Native

在庫レベルを更新する小売業者、顧客データにアクセスする営業担当者、または断続的な接続中にメッセージを送信するユーザーによってアプリが使用されることを想像してください。これらすべての場合において、オフライン機能は、シームレスなユーザー エクスペリエンスとイライラするユーザー エクスペリエンスの違いを意味する可能性があります。ここでオフラインファーストの考え方が役立ちます。

オフラインファーストのアプローチにより、インターネットが利用できない場合でもアプリは機能し続けることが保証されます。 WhatsApp のようなアプリは、この概念を完璧に示しています。オフライン中にメッセージを送信すると、メッセージはローカルに保存され、接続が復元されると自動的に送信されます。このシームレスなエクスペリエンスは、ローカル ストレージを活用し、ネットワーク ステータスを監視することによって実現されます。データベース経由であってもデバイス メモリ経由であっても、アプリは引き続き機能し、接続が再び利用可能になったときに保存されたデータをサーバーと同期します。

この記事では、ローカル ストレージ、データベース同期、Expo API を使用して React Native アプリケーションにオフライン サポートを実装する方法を説明します。オフラインファーストのアプローチには次のような利点があります。

  1. ユーザー エクスペリエンスの向上: ユーザーがダウンタイムを経験する可能性が低くなり、全体的な満足度が向上します。
  2. データの一貫性: データはローカルに保存され、オンライン時に同期されるため、データの損失や破損が防止されます。
  3. エンゲージメントの向上: オフラインで動作するアプリは柔軟性が高く、特にインターネットが不安定な地域では、エンゲージメントと維持率が向上します。

Expo と React Native によるオフライン サポートのセットアップ

Expo は、多くのプラットフォーム固有の構成を抽象化し、機能の構築に集中できるため、React Native 開発に最適なフレームワークです。このセクションでは、Expo、ローカル ストレージ用の AsyncStorage、ネットワーク ステータス検出用の NetInfo を使用して、シンプルな React Native アプリにオフライン サポートを実装する方法を検討します。

1. プロジェクトをセットアップする

まず、Expo を利用した新しい React Native プロジェクトを作成することから始めましょう。

npx create-expo-app offline-first-app
cd offline-first-app

ステップ 2: 依存関係のインストール

この例では、2 つの主要なライブラリを使用します:

@react-native-async-storage/async-storage: このライブラリを使用すると、デバイスにデータを保存できます。
@react-native-community/netinfo: このライブラリは、ネットワーク ステータスを検出し、デバイスがオンラインかオフラインかを判断するのに役立ちます。

必要なパッケージをインストールします:

expo install @react-native-async-storage/async-storage @react-native-community/netinfo

ステップ 3: オフライン ロジックの実装

次に、オンライン時に API からデータを取得し、オフライン時に使用できるようにローカルに保存する単純なアプリケーションを構築します。まず、App.js で基本構造を設定します。

import React, { useState, useEffect } from 'react';
import { StyleSheet, Text, View, Button, FlatList } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import NetInfo from '@react-native-community/netinfo';

const DATA_API = 'https://jsonplaceholder.typicode.com/posts';

export default function App() {
  const [data, setData] = useState([]);
  const [isOffline, setIsOffline] = useState(false);

  useEffect(() => {
    const loadData = async () => {
      // Check network status
      const netInfo = await NetInfo.fetch();
      setIsOffline(!netInfo.isConnected);

      if (netInfo.isConnected) {
        // Fetch data from API when online
        try {
          const response = await fetch(DATA_API);
          const result = await response.json();
          setData(result);

          // Cache the data for offline use
          await AsyncStorage.setItem('cachedData', JSON.stringify(result));
        } catch (error) {
          console.error('Failed to fetch data:', error);
        }
      } else {
        // Load data from AsyncStorage when offline
        try {
          const cachedData = await AsyncStorage.getItem('cachedData');
          if (cachedData) {
            setData(JSON.parse(cachedData));
          }
        } catch (error) {
          console.error('Failed to load data from cache:', error);
        }
      }
    };

    loadData();
  }, []);

  return (
    <View style={styles.container}>
      <Text style={styles.header}>Offline-First App</Text>
      <Text>Status: {isOffline ? 'Offline' : 'Online'}</Text>

      <FlatList
        data={data}
        keyExtractor={(item) => item.id.toString()}
        renderItem={({ item }) => (
          <View style={styles.item}>
            <Text style={styles.title}>{item.title}</Text>
          </View>
        )}
      />

      <Button title="Reload" onPress={() => loadData()} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: 50,
    paddingHorizontal: 20,
    backgroundColor: '#fff',
  },
  header: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 20,
  },
  item: {
    backgroundColor: '#f9c2ff',
    padding: 20,
    marginVertical: 8,
  },
  title: {
    fontSize: 16,
  },
});

どのように機能しますか?

  1. ネットワーク ステータスの検出: NetInfo ライブラリを使用して、デバイスがオンラインかオフラインかを確認します。オンラインの場合、アプリは API からデータを取得してキャッシュします。デバイスがオフラインの場合、アプリはキャッシュされたデータを AsyncStorage から取得します。

  2. データ キャッシュ: AsyncStorage を使用すると、オフライン アクセスのために API から取得したデータを保存できます。これは、アクティブなインターネット接続がなくてもアプリを機能させるために不可欠です。

  3. データ同期: 接続が復元されると、アプリは API から新しいデータを取得してキャッシュを更新し、オンラインのユーザーが常に最新の情報を入手できるようにします。

高度なオフライン機能と重要な考慮事項

次のようなより高度な機能を統合することで、この基本機能を構築できます。

  1. 同期戦略: 一部のアプリでは、競合が発生する可能性がある高度な同期戦略が必要です (例: 2 人のユーザーが同じデータをオフラインで更新する)。 PouchDB や Firebase などのツールは、リアルタイムのデータ同期と競合解決の管理に役立ちます。

  2. データベース ソリューション: より複雑なアプリの場合、大規模なデータセットやより高度なクエリを処理するために Realm や SQLite などのローカル データベースを使用することもできます。

  3. 楽観的な更新: 一部のアプリ、特にソーシャル メディアなどのユーザー作成コンテンツを含むアプリでは、ユーザーがオフラインでデータを作成、更新、削除できるようにするのが一般的です。 UI で変更が即座に行われ、アプリがインターネットに再接続したときにサーバーと同期される、楽観的な更新を実装できます。


Handling Complex Syncing and Conflict Resolution

In an offline-first app, conflicts arise when multiple users update the same data while offline and their changes are later synced with the server once the app reconnects to the internet. Handling these conflicts is crucial to maintain data consistency and provide a smooth user experience.

There are different strategies for resolving such conflicts, including:

  1. Last Write Wins (LWW)
  2. Manual Conflict Resolution
  3. Operational Transformation (OT)

I have some examples here for you to check.

1. Last Write Wins (LWW)

In this strategy, the most recent change (based on a timestamp) is accepted as the final value when syncing data. It is simple and works well for many applications, but it may lead to data loss if multiple users edit the same data.

Imagine you are building a note-taking app, if two users edit the same note while offline, the user who syncs their changes last will overwrite the previous user’s changes.

Let’s assume we have a local storage system (using AsyncStorage) and a remote server.

import AsyncStorage from '@react-native-async-storage/async-storage';

// Simulate syncing the note data with the server
const syncNoteWithServer = async (localNote) => {
  try {
    // Fetch the server data
    const response = await fetch('https://api.example.com/note');
    const serverNote = await response.json();

    // Compare timestamps
    if (localNote.updatedAt > serverNote.updatedAt) {
      // Local version is newer, so overwrite the server
      await fetch('https://api.example.com/note', {
        method: 'PUT',
        body: JSON.stringify(localNote),
        headers: { 'Content-Type': 'application/json' },
      });
    } else {
      // Server version is newer, discard local changes
      await AsyncStorage.setItem('note', JSON.stringify(serverNote));
    }
  } catch (error) {
    console.error('Sync failed:', error);
  }
};

// Example usage
const localNote = {
  content: 'This is an updated note.',
  updatedAt: Date.now(), // Timestamp of the last local update
};

syncNoteWithServer(localNote);

In this example:
The app compares the updatedAt timestamp of the local note (stored offline) with the note stored on the server.
If the local note is newer, it overwrites the server version. Otherwise, it discards local changes and updates the app with the server version.

Pros:

  1. Simple to implement.
  2. Works well for non-critical data.

Cons:

  1. May lead to data loss (e.g., if both users made significant changes).
2. Manual Conflict Resolution

With manual conflict resolution, the user is prompted to resolve conflicts when multiple versions of the same data exist. This approach is more user-friendly in scenarios where every change is valuable and users need to decide which data to keep.

Here is a potential case: In a collaborative editing app, two users edit the same document while offline. Once both versions are synced, the user is prompted to choose which version to keep or merge.

import AsyncStorage from '@react-native-async-storage/async-storage';
import { Alert } from 'react-native';

// Simulate syncing the document with the server
const syncDocumentWithServer = async (localDoc) => {
  try {
    // Fetch the server data
    const response = await fetch('https://api.example.com/document');
    const serverDoc = await response.json();

    if (localDoc.updatedAt !== serverDoc.updatedAt) {
      // Conflict detected, ask the user to resolve it
      Alert.alert(
        'Document Conflict',
        'Both you and another user have edited this document. Choose which version to keep.',
        [
          {
            text: 'Keep Local',
            onPress: async () => {
              // Overwrite the server with local changes
              await fetch('https://api.example.com/document', {
                method: 'PUT',
                body: JSON.stringify(localDoc),
                headers: { 'Content-Type': 'application/json' },
              });
            },
          },
          {
            text: 'Keep Server',
            onPress: async () => {
              // Discard local changes and update the app with the server version
              await AsyncStorage.setItem('document', JSON.stringify(serverDoc));
            },
          },
        ],
      );
    } else {
      // No conflict, proceed with syncing
      await AsyncStorage.setItem('document', JSON.stringify(serverDoc));
    }
  } catch (error) {
    console.error('Sync failed:', error);
  }
};

// Example usage
const localDoc = {
  content: 'This is my latest edit.',
  updatedAt: Date.now(), // Timestamp of the last local update
};

syncDocumentWithServer(localDoc);

Here's what's happening
If the updatedAt timestamps differ between the local and server versions, the app alerts the user and asks them to choose which version to keep. The user can decide whether to keep the local or server version.

Pros:

  1. Ensures that no important data is lost.
  2. Suitable for collaborative apps where user input is valuable.

Cons:

  1. Requires user intervention, which can be disruptive.
  2. May confuse non-technical users.

3. Operational Transformation (OT)
Operational Transformation is a more advanced technique used in real-time collaboration apps like Google Docs. It automatically merges conflicting changes by transforming operations in a way that preserves both sets of edits. OT allows multiple users to work on the same document simultaneously, and their changes are merged intelligently.

In a document editor app, two users edit different parts of a document. OT ensures that both sets of edits are applied without overwriting each other.

This implementation is a bit complex and require specialized libraries, such as ShareDB or Yjs. Here’s a basic pseudocode example of how OT works:

// Example of transforming two concurrent operations
const operation1 = { type: 'insert', position: 5, value: 'Hello' }; // User 1 adds 'Hello' at position 5
const operation2 = { type: 'insert', position: 3, value: 'World' }; // User 2 adds 'World' at position 3

const transformOperations = (op1, op2) => {
  // If both operations modify different positions, no conflict
  if (op1.position !== op2.position) return [op1, op2];

  // If operations conflict, adjust positions accordingly
  if (op1.position > op2.position) op1.position += op2.value.length;
  else op2.position += op1.value.length;

  return [op1, op2];
};

// Transform the operations to avoid conflicts
const [transformedOp1, transformedOp2] = transformOperations(operation1, operation2);

The positions of the two conflicting operations are adjusted so that they can both be applied without overwriting each other.

Pros:

  1. Ideal for real-time collaboration.
  2. Automatically resolves conflicts without user intervention.

Cons:

  1. Complex to implement.
  2. Requires specialized algorithms and libraries.

Conclusion
Each conflict resolution strategy comes with its trade-offs. For simpler apps, Last Write Wins may suffice. However, for collaborative apps where user data is crucial, Manual Conflict Resolution or more advanced techniques like Operational Transformation might be necessary. Choosing the right strategy depends on the complexity of your app and the importance of the data being modified.


I plan to create a series of articles that dive deeper into the following key topics:

Optimistic UI Updates – We'll explore how to immediately reflect changes made while offline in the UI, giving users the impression that their actions were successful. This approach greatly improves the user experience.

Web ベースのアプリに Service Worker を使用する – React Native Web 経由で Web 上にアプリをデプロイしている場合、Service Worker がプログレッシブ Web のオフライン キャッシュとバックグラウンド同期を有効にする方法について説明しますアプリ (PWA)。これにより、ユーザーはオフラインでもリソースやデータにアクセスできるようになります。

オフラインファースト アプリの実際の使用例 – Google マップ、Slack、Trello、Notion などのアプリがオフライン シナリオをどのように処理するかを詳しく見ていきます。これらの例を学ぶことで、オフラインファースト手法の実際の応用についてより深く理解できるようになります。

オフライン機能のテスト – オフライン機能のテストの重要性について説明し、ネットワーク中断をシミュレートするための React Native Debugger、Expo ツール、Network Link Conditioner (iOS 用) などのツールを確認します。また、Jest や React Native Testing Library などのライブラリを使用して、アプリがオフライン条件で正しく動作することを確認するテストを作成する方法も説明します。

パフォーマンスとストレージに関する考慮事項 – パフォーマンスは速度だけではありません。それはユーザーエクスペリエンスにも関係します。キャッシュされたデータを削減し、データの有効期限ポリシーを実装して、ローカル ストレージの過剰な使用を回避することで、パフォーマンスを最適化する戦略について説明します。

開発者の皆様、今後ともご期待ください。


最後まで読んでいただきありがとうございました。私は自分の学んだことを文書化して共有することを心から楽しんでいます。ビデオチュートリアルなどのコンテンツをさらに作成し、Instagram や TikTok で共有する予定です。初めての方のために説明すると、私は Zidane Gimiga です。ユーザー エクスペリエンスの最適化に情熱を注ぐソフトウェア開発者です。テクノロジーが私たちの生活にさらに統合されるにつれて、誰もがそれをできるだけ簡単に利用できるようにすることが不可欠です。より良い、ユーザーフレンドリーなソリューションを目指して努力を続けましょう。

ああ、私は Github にいます

以上がReact Native を使用したオフラインファースト アプリケーションの構築の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。