App Report 3 - ISIS-3510-Mobiles/Flutter GitHub Wiki

Team 24 - Dart

Highlights

1. Repo Link

https://github.com/ReVanced

For all of the videos listed in the report, you may access them through the following link: App Report 3 - Wiki

2. EC

This section evaluates the Eventual Connectivity (ECn) strategies in the ReVanced app, highlighting effective approaches and identifying potential anti-patterns that impact user experience. Eventual Connectivity strategies help the app manage connectivity issues, but poor implementation can lead to anti-patterns that disrupt usability or cause confusion. Each identified anti-pattern is supported by screenshots or videos, with explanations on how to reproduce the issue, its impact on the user experience, and suggestions for improvement.

Eventual Connectivity (EC) Strategies

The use of DioCacheInterceptor and DefaultCacheManager

Is a good EC strategy to handle temporary connectivity issues by caching responses and files, allowing the app to access previously loaded data even when offline. However, there could be further improvements, such as adding custom logic to manage cached data expiration or to notify users when viewing potentially outdated information from the cache.

Future<File> getSingleFile(String url) async {
 return DefaultCacheManager().getSingleFile(
     url,
     headers: {
         'User-Agent': _userAgent,
     },
   );
 }

you can see this ec strategy here: code

  • How to Reproduce: simulate offline mode by disconnecting the device from the internet and then triggering the getSingleFile method with a URL that has been previously fetched. If the app successfully loads the cached data, it demonstrates that the caching mechanism is functioning correctly.

  • Fix: Implementing cache expiration management would allow the app to determine how long cached data remains valid. Additionally, introducing user notifications to indicate when data may be outdated would help manage user expectations regarding the accuracy of the information displayed.

  • Why It’s Good Practice: utilizing caching mechanisms like DioCacheInterceptor and DefaultCacheManager is a good practice as it enables offline access to previously fetched data, improves application performance, and enhances responsiveness.

Refresh Indicator as EC Strategy

The RefreshIndicator widget allows users to refresh the content, which is a good strategy to maintain eventual connectivity. This feature provides a means to fetch fresh data even if the app was offline initially.

     body: RefreshIndicator(
     edgeOffset: 110.0,
     displacement: 10.0,
     onRefresh: () async => await model.forceRefresh(context),
     child: CustomScrollView(
       // ... slivers
       ),
      ),

you can see this ec strategy here: code

  • How to Reproduce: Pull down to refresh the content when offline. The app should display a loading indicator and attempt to fetch the latest data when connectivity is restored.

  • Fix: Ensure that the forceRefresh method in the ViewModel properly handles network availability checks before attempting to refresh the data.

  • Why It’s Good Practice: Implementing a refresh feature allows users to manually trigger data updates, providing a better experience during connectivity issues. It empowers users to control their content refresh rather than relying on automatic updates that may fail.

Anti-Patterns

Non-informative Error Handling

In initDio, changelog and clearAllCache, exceptions are caught and printed without providing informative feedback, which can leave users unaware of the underlying issue:

Dio initDio(String url) {
    var dio = Dio();
    try {
        dio = Dio(
            BaseOptions(
                baseUrl: url,
                headers: {
                    'User-Agent': _userAgent,
                },
            ),
        );
    } on Exception catch (e) {
        if (kDebugMode) {
            print(e);
        }
    }

    dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
    return dio;
}
  • You can see this anti-pattern here: code

  • How to Reproduce: Trigger an exception by showing changelog where theres no conection or by trying to clear the cache when no cache exists. Here's the example for showing changelog where the app doesn't tell what's the error or even shows an exception.

XRecorder_26102024_172005.mp4
  • Fix: Instead of just printing a generic exception or not saying anything at all like in this case, return an error message or show a custom exception message that provides more context. For example: There's currently no internet connection.

  • Why It’s Bad Practice: Non-informative error handling can lead to a poor user experience because users have no idea what went wrong or how to address it. This lack of clarity can lead to frustration and hinder app usability.

Bad installation handling

The current implementation does not show clear error handling or user feedback for when an error occurs during the installation process. It shows a generic error. This can leave users confused about what went wrong.

if (model.hasErrors) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text(t.installerView.errorMessage)),
  );
}
  • Potential Fix: Implement user notifications (e.g., using Snackbar) to inform users when errors occur or when the app is in a patching state. Provide more context or guidance on what users should do next when an error occurs.

  • How to Reproduce: Start the app and navigate to the InstallerView, then revoque permits of the app, run the install and observe that there is a generic error message displayed to the user.

image

  • Why It’s a Bad Practice: Failing to provide user feedback in the case of errors can lead to a frustrating user experience. Users may not understand what went wrong or how to rectify the issue, leading to decreased satisfaction with the app.

  • You can see this anti-pattern here: code

Dimmed UI Elements Confusion

Using the dimming effect applied to the PatchSelectorCard, could imply that the card is disabled or unavailable, leading to frustration as they are still able to interact with it. That's the case in the following example

Opacity(
  opacity: model.dimPatchesCard() ? 0.5 : 1,
  child: PatchSelectorCard(
    onPressed: model.dimPatchesCard()
        ? () => {}
        : () => model.navigateToPatchesSelector(),
  ),
),
  • Potential Fix: Use a clearer visual indicator (like a disabled state) and provide a message or tooltip to explain why the card is dimmed.

  • How to Reproduce: Observe the PatchSelectorCard when it’s dimmed and attempt to interact with it, then the dimming does not clearly indicate that the interaction is restricted, you may be unsure how to proceed. None of the grayed patches are available yet the user can still interact with them.

image

  • Why It’s a Bad Practice: Dimming the PatchSelectorCard can mislead users into thinking it is disabled while still allowing interaction, causing frustration and confusion. A clearer visual indicator is needed to improve user understanding and experience.

  • You can see this anti-pattern here: code

3. Caching Strategies

The caching strategies used in ReVanced are essential for optimizing data loading and enhancing overall performance, especially for repetitive tasks like loading images and network data. The caching strategies used in ReVanced Manager are detailed below.

a. Caching Structures Used by ReVanced

ReVanced Manager implements several caching structures, focusing on optimizing data retrieval and reducing network load. The main caching structures identified are:

  1. flutter_cache_manager
    This Flutter package provides a comprehensive cache solution for file and image storage, allowing network resources to be stored locally.
    Location: ReVanced Manager pubspec.yaml

  2. Dio Cache Interceptor
    Used in combination with the dio HTTP client, dio_cache_interceptor caches network responses. This allows recurring HTTP requests to retrieve data directly from the cache instead of making new calls.
    Location: ReVanced Manager pubspec.yaml

  3. RAM for Temporary Storage
    RAM is used to temporarily cache non-persistent data, such as UI configurations and API responses that do not need durable storage.
    Location: ReVanced Manager - any ViewModel state management file

b. Code Snippets and Explanation

  1. flutter_cache_manager
    flutter_cache_manager is used to cache downloaded images and files, optimizing data usage and loading speed on mobile devices.
    import 'package:flutter_cache_manager/flutter_cache_manager.dart';
    
    Future<File> getSingleFile(String url) async {
       return DefaultCacheManager().getSingleFile(
         url,
       headers: {
          'User-Agent': _userAgent,
        },
      );
    }
    
    Stream<FileResponse> getFileStream(String url) {
      return DefaultCacheManager().getFileStream(
        url,
        withProgress: true,
        headers: {
          'User-Agent': _userAgent,
        },
      );
    }
    

The method listed before is found in download_manager.dart. This method helps by caching one or multiple files locally, to allow resource reuse without requiring repeated downloads.

  1. Dio Cache Interceptor This interceptor enables caching of HTTP responses, allowing for quick retrieval of API data without multiple network requests.
    import 'package:dio/dio.dart';
    import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
    [...]
    Dio initDio(String url) {
      var dio = Dio();
      try {
        dio = Dio(
          BaseOptions(
            baseUrl: url,
            headers: {
              'User-Agent': _userAgent,
            },
          ),
        );
      } on Exception catch (e) {
        if (kDebugMode) {
          print(e);
        }
      }
    
      dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
      return dio;
    }
    

The method before is found in download_manager.dart. This method allows that, as Network data is stored temporarily, repeated network calls fetch information from the cache. This is especially helpful for APIs with infrequent updates.

  1. Memory Caching in ViewModels In Flutter view models, temporary data such as UI configurations and values are stored in memory to reduce the load on persistent storage.
    import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
    import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
    [...]
    class PatchOptionsViewModel extends BaseViewModel {
      final ManagerAPI _managerAPI = locator<ManagerAPI>();
      final String selectedApp =
          locator<PatcherViewModel>().selectedApp!.packageName;
      List<Option> options = [];
      List<Option> savedOptions = [];
      List<Option> modifiedOptions = [];
    
      Future<void> initialize() async {
        options = getDefaultOptions();
        for (final Option option in options) {
          final Option? savedOption = _managerAPI.getPatchOption(
            selectedApp,
            _managerAPI.selectedPatch!.name,
            option.key,
          );
          if (savedOption != null) {
            savedOptions.add(savedOption);
          }
        }
        modifiedOptions = [
          ...savedOptions,
          ...options.where(
            (option) => !savedOptions.any((sOption) => sOption.key == option.key),
          ),
        ];
      }
    

The method listed before is found in patch_options_viewmodel.dart. Temporary data such as user options selected are stored in memory, enabling quick access without needing persistent storage. This method lots of others ViewModels in ReVanced.

c. Analysis of the Choice of Caching Structures

The caching structures selected improve the performance of the ReVanced Manager app:

  • flutter_cache_manager: Ideal for managing images and files, reducing network load and improving app loading speeds on mobile devices.
  • Dio Cache Interceptor: This interceptor helps the app reduce repeated HTTP requests significantly, saving bandwidth and improving application responsiveness.
  • Memory Caching in ViewModels: This method is suitable for temporarily storing data that does not need persistence, such as configurations or UI values. RAM minimizes persistent cache use and ensures quick data retrieval.

d. Analysis of Average Cache Storage

The average cache storage in ReVanced Manager primarily depends on resources stored in flutter_cache_manager and the size of network responses cached in dio_cache_interceptor. On devices with storage constraints, cache usually occupies less than 50 MB, optimizing resource reuse without taking up significant space.

4. Memory Management Strategies

For the ReVanced Manager app, memory management is essential to ensure optimal performance, given the app's complexity in handling patches, background tasks, and user interactions. Here's an evaluation of the memory management strategies used:

a. Memory Mechanisms Used by the App

ReVanced Manager uses the following memory management strategies:

  1. State Management with Stacked Architecture:

    • The app uses the Stacked package for managing state. It separates logic from UI components, ensuring that state changes do not cause unnecessary rebuilds, thus optimizing memory usage.
  2. Dart Garbage Collection:

    • Dart uses automatic garbage collection to manage memory. The app relies on Dart’s built-in garbage collector to release memory when objects are no longer referenced, which helps maintain low memory usage.
  3. Caching Mechanisms:

    • The app uses caching strategies via the flutter_cache_manager and dio_cache_interceptor libraries. These are used to store network responses temporarily, reducing memory overhead by avoiding repeated network calls.

b. Code Snippets and Explanation

  1. State Management (Stacked):

    • In the PatchesSelectorView code, the app uses the ViewModelBuilder from the Stacked package to manage the state of the view. It efficiently updates the UI only when necessary.
    return ViewModelBuilder<PatchesSelectorViewModel>.reactive(
      onViewModelReady: (model) => model.initialize(),
      viewModelBuilder: () => PatchesSelectorViewModel(),
      builder: (context, model, child) => Scaffold(
        // UI components here
      ),
    );
    • Functionality: The ViewModel isolates the state logic from the view, ensuring efficient memory use by updating only when needed.
  2. Caching (flutter_cache_manager and dio_cache_interceptor):

    • The app uses caching for network requests. Here’s a sample configuration using the dio_cache_interceptor:
    final dio = Dio();
    dio.interceptors.add(DioCacheInterceptor(
      options: CacheOptions(
        store: MemCacheStore(), // Memory cache store
        policy: CachePolicy.request, // Cache the responses
      ),
    ));
    • Functionality: This mechanism is associated with storing API responses temporarily to avoid repeated network calls, saving memory and processing power.
  3. Garbage Collection (Dart’s GC):

    • The app leverages Dart’s automatic garbage collection, but it’s not explicitly coded. The GC ensures that objects no longer in use (like temporary variables or detached widgets) are cleaned up automatically.
    • Functionality: This is inherently tied to all aspects of the app, such as managing UI components (widgets) and data objects, ensuring memory is freed when they’re no longer needed.

c. Functionality Associated with Each Mechanism

  • State Management (Stacked): Manages UI updates and logic separation, ensuring efficient use of memory while interacting with the UI.
  • Caching: Improves performance and reduces memory consumption by caching frequently used data like network responses.
  • Garbage Collection: Provides overall memory management by clearing unused objects in the background, which is essential for the app’s smooth performance.

d. Persisted Data Analysis

  1. Persistent vs. Transient Data:

    • Persistent Data: User settings, patch selections, and configuration preferences should be stored persistently using local storage (e.g., shared_preferences or a database) to maintain data across sessions.
    • Transient Data: API responses or UI state information that only needs to exist during the app’s active session can be stored in memory (RAM) temporarily.
  2. Private vs. Public Space:

    • Private Space: Sensitive data (like user settings or cached API responses) should be stored in the app’s private space to ensure it remains secure and inaccessible to other apps.
    • Public Space: If the app stores less sensitive, large data files (e.g., downloaded patch files), it could use a public directory, making it accessible for other tools or file management apps.

By using these memory management strategies, ReVanced Manager ensures it remains efficient and responsive while keeping memory usage low and maintaining user data security.

5. Threading/concurrency Strategies

Thread and concurrency management in ReVanced applications is very important for optimizing performance and enhancing the user experience while applying patches and handling the UI. Below are the concurrency strategies observed in the source code.

a. Concurrency Strategies Used by ReVanced

ReVanced applies various concurrency strategies, particularly in ReVanced Manager and ReVanced Patcher modules. Key strategies include:

  1. Background Threading with Garbage Collection (GC) in Concurrent Tasks
    To reduce UI impact, garbage collection (GC) runs in background threads. The revanced-manager code includes concurrent copying GC functions, which optimize memory without affecting the user experience.

  2. Resource Locking for Memory Synchronization
    ReVanced uses locks to coordinate access to shared resources, such as in blocking garbage collection (blocking GC Alloc), ensuring threads do not conflict when releasing or modifying memory.

  3. Rendering Synchronization with Flutter in Separate Threads
    In Flutter-built interfaces, separate threads manage rendering and graphic synchronization through functions like ViewRootImpl, allowing real-time graphic updates without UI performance degradation.

  4. Memory Leak Prevention in Concurrent Video Playback
    During video playback, there is a concurrent usage of pathBuilder that can lead to memory leaks, which may be prevented by resetting shared objects to prevent memory accumulation.

b. Code Snippets and Explanation

  1. Concurrent Garbage Collection (GC) in Background Threads
    In revanced-manager, concurrent copying GC runs to free memory in the background. The concurrent GC is implemented automatically by the Dart runtime and it's not needed for developers to manage this with explicit code. This background GC feature is used in order to process free memory without affecting the interface, optimizing the user experience.

  2. Blocking Resource Lock for Memory Allocation To avoid conflicts when accessing shared resources, resource locking is implemented in revanced-manager.

    import 'package:synchronized/synchronized.dart';
    [...]
    Future<Response> _dioGetSynchronously(String path) async {
      // Create a new Lock for each path
      if (!_lockMap.containsKey(path)) {
        _lockMap[path] = Lock();
      }
      return _lockMap[path]!.synchronized(() async {
        return await _dio.get(path);
      });
    }

This method in particular is used in github_api.dart and is used in order to avoid conflicts when accessing shared resources.

  1. Graphical Rendering Synchronization with Flutter (ViewRootImpl) Flutter code handling UI graphics employs ViewRootImpl to execute tasks on independent rendering threads. Just like the concurrent GC, this method is already implemented within the Dart runtime and there's no explicit code. This solution helps to keep graphical operations on separate threads which keeps the UI responsive, even when handling complex graphics.

  2. Memory Leak Prevention in Concurrent Video Playback In revanced-integrations, solutions are proposed to prevent memory buildup during video playback.

    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    object ResourceMappingPatch : ResourcePatch() {
      private val resourceMappings = Collections.synchronizedList(mutableListOf<ResourceElement>());
    
      private val THREAD_COUNT = Runtime.getRuntime().availableProcessors();
      private val threadPoolExecutor = Executors.newFixedThreadPool(THREAD_COUNT);
      [...]
    }

This method used is found in the revanced-patches location, and it may be used in order to prevent the amount of objects in memory to produce leaks by keeping a fixed size, optimizing system load.

c. Functionalities Associated with Each Strategy

  • Concurrent Garbage Collection (GC): Optimizes memory usage during intensive operations without blocking the interface, maintaining application performance.
  • Blocking Resource Lock: Ensures synchronization in tasks that access shared memory, avoiding conflicts over resources.
  • Graphical Rendering Synchronization: Guarantees that graphical updates do not slow down the UI, especially useful in Flutter-built interfaces.
  • Memory Leak Prevention in Video Playback: Minimizes memory usage during video playback, avoiding slowdowns and performance issues.
⚠️ **GitHub.com Fallback** ⚠️