Offline-friendly react-native apps

Опыте внедрения поддержки оффлайн в приложении

https://david-talks.netlify.com/offline

Offline-friendly react-native apps

План:

Подходы

Не упоминать о оффлайне

Ничего не показывать

Заявить о потребностях ...

Возможно не стоит...

  1. Постоянно напоминать о оффлйне;
  2. Блокировать доступ к критическим данным;
  3. Забывать о юзере (разлогин);
  4. Использовать NetInfo для проверки оффлайн режима.

NetInfo — это не про оффлайн!

How quickly add offline support

Offline images ✔️

С чего начали

  1. Api layer (testing, refresh tokens);
  2. Normalization (JSON API heavy response);
  3. Serialization (json, dates fields);
  4. Cache manager;
  5. Clean strategy.

Реализация


          class StorageResource<Entity extends { id: ItemId }> {
              constructor({ type, serializer }): StorageResource;

              getItems(ids: Array<string>): {[type: string]: Array<Entity>} | null;
              multiRemove(ids: Array<ItemId>): Promise<void>;
              setItems(items: Array<Entity>): Promise<void>;

              cleanAll(): Promise<void>;
              cleanUp(options: { days: number }): Promise<void>;

              parseItem(rawItem: string): Entity;
              serializeItem(item: Entity): string;
              trackLastRead(ids: Array<ItemId>): Promise<void>;
            }
        

          const usersStorage = StorageResource({ type: 'users', serializer });

          usersStorage.setItems([{ id: 1, name: 'Davyd' }]);

          /*
          AsyncStorage: {
              'CV::users::1': '{"id":1,"name":"Davyd"}',
              'LR::users::1': '1573942810000',
          }
          */

          usersStorage.getItems([1]);
          // AsyncStorage: {'LR::users::1': '1573942820000',}

          usersStorage.getItems([1]);
          // AsyncStorage: {'LR::users::1': '1573942830000',}
        

В реальности общение с API не обходиться только id

GET /current/user
GET /data?include=magic

          class StorageResourceWithHelpers<
            Entity extends { id: ItemId }
          > extends StorageResource<Entity> {
            setCustomIds(key: string, ids: Array<ItemId>): Promise<void>;
            getCustomIds(key: string): Promise<Array<ItemId> | null>;
            removeCustomKeys(keys: Array<string>): Promise<void>;
          }
        

          class BlocksResource extends StorageResourceWithHelpers(){
              async getStoredMainScreenBlocks(){
                  const ids = this.getCustomIds('main-screen-blocks');

                  return ids ? this.getItems(ids) : null;
              }
          }

          const resource = BlocksResource({ type: 'blocks', serializer });
          //...
          const [ids,items] = await apiMethod(...);

          resource.setCustomIds('main-screen-blocks', ids);
          resource.setItems(items);
          resource.getStoredMainScreenBlocks(); // {blocks: [...]}
        

          const handlerOfflineRejects = async (...args: Args) => {
              try {
                  await apiMethod(...args).then((ids) => onGetIds(ids));
                  // ...
              } catch (error) {
                  if (isNetworkError(error)) {
                      const resourcesIndex = await storageMethod();

                      if (resourcesIndex) {
                          dispatch(indexResources(resourcesIndex));
                          // ...
                      }
                  }

                  throw error;
              }
          };
        

Using offline fallback


          export const findCurrentCompany = createOfflineFallbackSaga({
            apiMethod: () => api.getCurrentCompany(),
            storageMethod: () => storage.getStoredCurrentCompany(),
            onGetIds: (ids: Array<CompanyId>) => storage.setCurrentCompanyIds(ids),
          });
        

Handle errors

Demo time...

Доверяйте вашему устройтву, не соединению

Будьте пессимистичны \ оптимистичны и прагматичны в ваших интерфейсах

Вопросы ?