MixStack lets you connects Flutter smoothly with Native pages, supports things like Multiple Tab Embeded Flutter View, Dynamic tab changing, and more. You can enjoy a smooth transition from legacy native code to Flutter with it.

Overview

alt text

中文 README

MixStack

MixStack lets you connects Flutter smoothly with Native pages, supports things like Multiple Tab Embeded Flutter View, Dynamic tab changing, and more. You can enjoy a smooth transition from legacy native code to Flutter with it.

MixStack basic structure

alt text

As above picture shows, each Native Flutter Container provided by MixStack, contains an independent Flutter Navigator to maintain page management, through Stack Widget inside Flutter to let Flutter render the current Native Flutter Container belonged Flutter page stack. With this approach, Flutter and Native's page navigation, and all kind of View interaction became possible and manageable.

Get Start

Hybrid development may sometimes a bit overwhelming and bit complicated, please have some patience.

Installation

Add mixstack dependency in pubspec.yaml

dependencies:
  mix_stack: <lastest_version>

Run flutter pub get in your project folder

Run pod install in your iOS folder

Add in Android's build.gradle:

implementation rootProject.findProject(":mix_stack")

How to Integrate

On Flutter Side

Find your root Widget inside your Flutter project, and add MixStackApp in your initial Widget's build, pass your route generation function to MixStackApp, like below:

class MyApp extends StatelessWidget {
  void defineRoutes(Router router) {
    router.define('/test', handler: Handler(handlerFunc: (ctx, params) => TestPage()));
  }

  @override
  Widget build(BuildContext context) {
    defineRoutes(router);
    return MixStackApp(routeBuilder: (context, path) {
      return router.matchRoute(context, path).route; //传回 route
    });
  }
}

Flutter part is done。For detail usage please check Flutter side usage in detail

On iOS Side

After FlutterEngine excute Run,set engine to MXStackExchange

//AppDelegate.m
[flutterEngine run];
[MXStackExchange shared].engine = flutterEngine;

iOS part is done。For detail usage please check iOS side usage in detail

On Android Side

Make sure in Application's onCreate() execute:

MXStackService.init(application);

Android part is done。For detail usage please check Android side usage in detail

Flutter Side Usage

Listen to container's Navigator

If you need to listen to navigator inside container, you can add additional observer builder in MixStackApp initialization.

MixStackApp(
      routeBuilder: (context, path) {
        return router.matchRoute(context, path).route;
      },
      observersBuilder: () {
        return [CustomObserver()];
      },
    )

Control native UI display

When you have native view mixed with your Flutter container, sometimes you may want to hide those native views such like you push a new page inside Flutter, something like picture shows: alt text

You can do so by using NativeOverlayReplacer.

When you need to achieve this, you just need to wrap your page's root widget with NativeOverlayReplacer, and fill in the Native Overlay view's name that you registered in native.

@override
Widget build(BuildContext context) {
  return NativeOverlayReplacer(child:Container(), autoHidesOverlayNames:[MXOverlayNameTabBar, 'NativeOverlay1', 'NativeOverlay2']);
}

We also offer a simple interface to hide MixStack offered Native Tab to simplify the common needs that you embedded a Fluter tab and that tab have its own navigation stack, and when pushed, you need to hide native tab bar.

@override
Widget build(BuildContext context) {
  return NativeOverlayReplacer.autoHidesTabBar(child:Container());
}

If you want fine tuned control, you can tweak the persist attribute

List<String> names = await MixStack.getOverlayNames(context); //Get current native overlay names
names = names.where((element) => element.contains('tabBar')).toList();
NativeOverlayReplacer.of(context).registerAutoPushHiding(names, persist: false); //If we hope this register to work once for single push action, we set persist false, if we want it to work everytime we push a page, set it to true

After above code setting, everytime a page pushing action will trigger setting of native UI components, also offer a native UI components snapshot inside Flutter to offer smooth animation and let user ignore the hybrid structure.

Direct pop the current Flutter container

MixStack.popNative(context);

Force adjust current Flutter container's back gesture status

MixStack handle this very well all the time, but sometimes you may needs this capability, make sure you don't shoot your foot.

MixStack.enableNativePanGensture(context, true);

Flutter respond to current container event

Sometimes you may need Flutter code to respond to specific native call, and this achieves that.

//Some Widget code
void initState() {
  super.initState();
  //Root page register a navigator popToRoot action
  if (!Navigator.of(context).canPop()) {
    PageContainer.of(context).addListener('popToTab', (query) {
      Navigator.of(context).popUntil((route) {
        return route.isFirst;
      });
    });
  }
}

Manually adjust Native Overlay

NativeOverlayReplacer.of(ctx)
                .configOverlays([NativeOverlayConfig(name: MXOverlayNameTabBar, hidden: false, alpha: 1)]);

Advance: Subsribe to Flutter App lifecycle

MixStack offers whole Flutter App lifecycle for listen, due to some structure difference than original Flutter lifecycle, if you need to do visibility check or other stuff, consider using this.

    MixStack.lifecycleNotifier.addListener(() {
      print('Lifecycle:${MixStack.lifecycleNotifier.value}');
    });

Advance: Subscribe to current container's SafeAreaInsets change

If Flutter side UI components wants to know Native container insets change, you can do the follows:

  PageContainerInfo containerInfo;
  @override
  void initState() {
    super.initState();
    //Get current container info, and subscribe changes
    containerInfo = PageContainer.of(context).info;
    bottomInset = containerInfo.insets.bottom;
    containerInfo.addListener(updateBottomInset);
  }

  @override
  void dispose() {
    //Cancel subscription
    containerInfo.removeListener(updateBottomInset);
    super.dispose();
  }

  updateBottomInset() {
    bottomInset = PageContainer.of(context).info.insets.bottom;
  }

Beware that this subscription only passing SafeAreaInsets change, if you want to know more about Native Container, use MixStack.overlayInfos to get infos.

Advance: Get current container's native overlay attributes for more customization

  double bottomInset = 0;
  @override
  Widget build(BuildContext context) {
    //Get native overlay infos to adjust UI inset
    MixStack.overlayInfos(context, [MXOverlayNameTabBar], delay: Duration(milliseconds: 400)).then((value) {
      if (value.keys.length == 0) {
        return;
      }
      final info = value[MXOverlayNameTabBar];
      double newInset = info.hidden == false ? info.rect.height : 0;
      if (bottomInset != newInset) {
        setState(() {
          bottomInset = newInset;
        });
      }
    });
    return Stack(
          children: [
            Positioned(
              bottom: bottomInset + 10,
              right: 20,
              child: FloatingActionButton(
                onPressed: () {
                  print("Floating button follow insets move");
                },
              ),
            ),
          ],
        );
  }

iOS side usage

Put multiple Flutter page in TabBarController

MixStack offsers MXAbstractTabBarController for subclass,mainly for adjusting tab insets and handle tabbar visibility.If you wants more, you can implement one yourself.

When you want to add one or more Flutter Views, please use MXContainerViewController as Tab's child viewController. If somehow your MXContainerViewController was gonna embedded inside another VC, please mark the root VC with our custom tag

vc.containsFlutter = YES;

The example code of adding Flutter Pages into Tab shows like below:

TabViewController *tabs = [[TabViewController alloc] init];
UITabBarItem *item1 = [[UITabBarItem alloc] initWithTitle:@"Demo1" image:[UIImage imageNamed:@"icon1"] tag:0];
UITabBarItem *item2 = [[UITabBarItem alloc] initWithTitle:@"Demo2" image:[UIImage imageNamed:@"icon2"] tag:0];
MXContainerViewController *flutterVC1 = [[MXContainerViewController alloc] initWithRoute:@"/test" barItem:item1];
MXContainerViewController *flutterVC2 = [[MXContainerViewController alloc] initWithRoute:@"/test_2" barItem:item2];
SomeNativeVC *nativeVC = [[SomeNativeVC alloc] init]; //Of course you can add normal native VC ^_^
[tabs setViewControllers:@[flutterVC1, flutterVC2, nativeVC]];

Flutter Container as normal VC

MixStack's MXContainerViewController can use in normal scene.Just passing the Flutter route matching the target page.

MXContainerViewController *flutterVC = [[MXContainerViewController alloc] initWithRoute:@"/test"]
[self.navigationController pushViewController:flutterVC animated:YES];

Pop Flutter Container

Assume current page is Flutter Container,and you're not sure about whether you need to pop this VC or you need to pop one of the page contains in the container, you can try like below:

BOOL result = [[MXStackExchange shared] popPage];

If result is true, means inside Flutter Container's navigation stack there's a page being popped, and there's still pages there. If result is false, then it means that current Container contains navigation stack only have root page left, so you can pop the whole container safely.

Submit event to Flutter Container's Flutter stack

MXContainerViewController *flutterVC = (MXContainerViewController *)self.tab.viewControllers.lastObject;
[flutterVC sendEvent:@"popToTab" query:@{ @"hello" : @"World" }];

Get current Flutter Container's navigation history

If returns nil, that means current Container contains zero page.

MXContainerViewController *flutterVC = ...;
[flutterVC currentHistory:^(NSArray<NSString *> *_Nullable history) {
    NSLog(@"%@", history);
}];

Lock current container engine rendering

Sometimes we need to show some UI above Flutter Container, like some popup window.Due to Flutter rendering mechanism, when you do that, Flutter View will black out. So we offer a snapshot mechanism, when you set showSnapshot to true, the whole view will be snapshoted and freeze.When you done your business, you can set it back to false.

MXContainerViewController *fc = ...
fc.showSnapshot = YES;

iOS advance usage & Q&A

Potential problems in multiple Window usage

In some circumstance, iOS side may use multiple window way to manage UI, it may happened that two window both contains Flutter Container, and it is knowned that different window's VC won't receive callback for visibility. When you meets this situation, you can use codes like below to manually guide MixStack to put FlutterEngine's viewController back to the business correct one.

[MXStackExchange shared].engineViewControllerMissing = ^id<MXViewControllerProtocol> _Nonnull(NSString *_Nonnull previousRootRoute) {
  return someFlutterVCFitsYourBusiness;
};

Custom support for NativeOverlayReplacer

Make sure the Flutter Container VC implement MXOverlayHandlerProtocol 。 You can check related example code in MXAbstractTabBarController

What is ignoreSafeareaInsetsConfig

In MixStack's MXOverlayHandlerProtocol, there's one method called ignoreSafeareaInsetsConfig, this method based on a fact that, in most circumstance, MixStack suggest to set overlay causing SafeAreaInsets to zero,that is to say, ,Flutter rendering layer should know nothing about native UI's SafeAreaInsets change, the reason for this suggestion is that SafeAreaInsets changing can cause Flutter re-render everything, for complex UI, this is costy and may cause weired bug. So we suggest in specific Container you implement ignoreSafeareaInsetsConfig, then inside Flutter use MixStack.of(context).overlayInfos to get the overlay changes info for UI adjustment.

Android Usage

MXFlutterActivity usage

We offer MXFlutterActivity for direct use, just passing target page route registered in Flutter

Intent intent = new Intent(getActivity(), MXFlutterActivity.class);
intent.putExtra(ROUTE, "/test_blue"); //Passing the targeted page route registered in Flutter
startActivity(intent);

MXFlutterFragment usage inside activity and Tab usage

We offer MXFlutterFragment for fragment usage, it's like MXFlutterActivity, also need passing target page route registered in Flutter

MXFlutterFragment flutterFragment = new MXFlutterFragment();
Bundle bundle = new Bundle();
bundle.putString(MXFlutterFragment.ROUTE, "/test_blue");
hxFlutterFragment.setArguments(bundle);

For Tab switching,we need controls over MXFlutterFragment,for MXFlutterFragment 's host Activity , you need to implement IMXPageManager interface, it's only one function, getPageManager(), mainly for getting MXPageManager in Activity, this serves two purpose:

  • Controls MXFlutterFragment page lifecycle

  • Offer Flutter page's native UI control capability

Controls MXFlutterFragment page lifecycle

Classic situation: theres's multiple Tab in activity, each point to different Fragment, you need IMXPageManagerto control which Fragment to display and also set the MXFlutterFragment lifecycle right, as below:

private void showFragment(Fragment fg) {
    FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
    if (currentFragment != fg) {
        transaction.hide(currentFragment);
    }
    if (!fg.isAdded()) {
        transaction.add(R.id.fl_main_container, fg, fg.getClass().getName());
    } else {
        transaction.show(fg);
    }
    transaction.commit();
    currentFragment = fg;
  
    pageManager.setCurrentShowPage(currentFragment); //tell  MixStack which MXFlutterFragment to show
}

Since Flutter have different mechanism with Android, we need to override back logic. So in MXFlutterFragment's host Activity we need to add following:

@Override
public void onBackPressed() {
    if (pageManager.checkIsFlutterCanPop()) {
        pageManager.onBackPressed(this);
    } else {
        super.onBackPressed();
    }
}

When Activity gets destroy, we need to also notify MixStack through IMXPageManager:

@Override
protected void onDestroy() {
    super.onDestroy();
    pageManager.onDestroy();
}

Managing Flutter Container's native UI

We also use PageManager to achieve this, you can directly intialize one if you don't need to manage native UI, otherwise you need override methods, there's four methods needs to override:

  • overlayViewsForNames get the mapping between view and names
  • configOverlay Config how native overlay view display, aniate, there's default animation
  • overlayNames get avaiable names
  • overlayView get overlay views through name

the example are shown below

MXPageManager pageManager = new MXPageManager() {
  @Override
  public List<String> overlayNames() {
    List<String> overlayNames = new ArrayList<>();
    overlayNames.add("tabBar");
    return overlayNames;
  }

  @Override
  public View overlayView(String viewName) {
    if ("tabBar".equals(viewName)) {
      return mBottomBar;
    }
    return null;
  }
};

Submit events to FlutterFragment/FlutterActivity

flutterFragment.sendEvent("popToTab", query);
flutterActivity.sendEvent("popToTab", query);

Get Flutter Container's navigation stack history

pageManager.getPageHistory(new PageHistoryListener() {
    @Override
    public void pageHistory(List<String> history) {
    }
});

Destroy

Destory when your MainActivity onDestroy gets called

MXStackService.getInstance().destroy();
You might also like...

Code metrics for Java code by means of static analysis

CK CK calculates class-level and method-level code metrics in Java projects by means of static analysis (i.e. no need for compiled code). Currently, i

Jan 4, 2023

A Java library to query pictures with SQL-like language

A Java library to query pictures with SQL-like language

PicSQL A Java library to query pictures with SQL-like language. Features : Select and manipulate pixels of pictures in your disk with SQL-like dialect

Dec 25, 2022

A Java library to query pictures with SQL-like language.

A Java library to query pictures with SQL-like language.

PicSQL A Java library to query pictures with SQL-like language. Features : Select and manipulate pixels of pictures in your disk with SQL-like dialect

Dec 25, 2022

esProc SPL is a scripting language for data processing, with well-designed rich library functions and powerful syntax, which can be executed in a Java program through JDBC interface and computing independently.

esProc SPL is a scripting language for data processing, with well-designed rich library functions and powerful syntax, which can be executed in a Java program through JDBC interface and computing independently.

esProc esProc is the unique name for esProc SPL package. esProc SPL is an open-source programming language for data processing, which can perform comp

Dec 27, 2022

A plugin for the ja-netfilter, it can replace strings dynamically.

plugin-mymap A plugin for the ja-netfilter, it can replace strings dynamically. Use the mvn clean package command to compile and use mymap-v1.0.1-jar-

Dec 12, 2022

eXist Native XML Database and Application Platform

eXist Native XML Database and Application Platform

eXist-db Native XML Database eXist-db is a high-performance open source native XML database—a NoSQL document database and application platform built e

Dec 30, 2022

Provides many useful CRUD, Pagination, Sorting operations with Thread-safe Singleton support through the native JDBC API.

Provides many useful CRUD, Pagination, Sorting operations with Thread-safe Singleton support through the native JDBC API.

BangMapleJDBCRepository Inspired by the JpaRepository of Spring framework which also provides many capabilities for the CRUD, Pagination and Sorting o

Apr 7, 2022

Microstream - High-Performance Java-Native-Persistence

Microstream - High-Performance Java-Native-Persistence. Store and load any Java Object Graph or Subgraphs partially, Relieved of Heavy-weight JPA. Microsecond Response Time. Ultra-High Throughput. Minimum of Latencies. Create Ultra-Fast In-Memory Database Applications & Microservices.

Dec 28, 2022

Java code generator for calling PL/SQL.

OBridge OBridge provides a simple Java source code generator for calling Oracle PL/SQL package procedures. Supported input, output parameters and retu

Oct 7, 2022
Comments
  • FlutterMainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'io.flutter.plugin.common.BinaryMessenger io.flutter.embedding.engine.plugins.FlutterPlugin$FlutterPluginBinding.getBinaryMessenger()' on a null object reference

    FlutterMainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'io.flutter.plugin.common.BinaryMessenger io.flutter.embedding.engine.plugins.FlutterPlugin$FlutterPluginBinding.getBinaryMessenger()' on a null object reference

    io.flutter.embedding.engine.plugins.FlutterPlugin$FlutterPluginBinding.getBinaryMessenger()' on a null object reference
    E/CrashReport(20510): 	at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4545)
    E/CrashReport(20510): 	at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4577)
    E/CrashReport(20510): 	at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52)
    E/CrashReport(20510): 	at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
    E/CrashReport(20510): 	at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
    E/CrashReport(20510): 	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2142)
    E/CrashReport(20510): 	at android.os.Handler.dispatchMessage(Handler.java:106)
    E/CrashReport(20510): 	at android.os.Looper.loop(Looper.java:236)
    E/CrashReport(20510): 	at android.app.ActivityThread.main(ActivityThread.java:7864)
    E/CrashReport(20510): 	at java.lang.reflect.Method.invoke(Native Method)
    E/CrashReport(20510): 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:620)
    E/CrashReport(20510): 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1011)
    E/CrashReport(20510): Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'io.flutter.plugin.common.BinaryMessenger io.flutter.embedding.engine.plugins.FlutterPlugin$FlutterPluginBinding.getBinaryMessenger()' on a null object reference
    E/CrashReport(20510): 	at com.crazecoder.openfile.OpenFilePlugin.onAttachedToActivity(OpenFilePlugin.java:400)
    E/CrashReport(20510): 	at io.flutter.embedding.engine.FlutterEngineConnectionRegistry.attachToActivityInternal(FlutterEngineConnectionRegistry.java:351)
    E/CrashReport(20510): 	at io.flutter.embedding.engine.FlutterEngineConnectionRegistry.attachToActivity(FlutterEngineConnectionRegistry.java:324)
    E/CrashReport(20510): 	at com.yuewen.mix_stack.component.ActivityFragmentDelegate.onAttach(ActivityFragmentDelegate.java:106)
    E/CrashReport(20510): 	at com.yuewen.mix_stack.component.MXFlutterActivity.onResume(MXFlutterActivity.java:102)
    E/CrashReport(20510): 	at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1456)
    E/CrashReport(20510): 	at android.app.Activity.performResume(Activity.java:8264)
    E/CrashReport(20510): 	at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4535)
    E/CrashReport(20510): 	... 11 more
    
    opened by adger-me 3
  • 构造器 FlutterView.FlutterView(Context,RenderMode)不适用 是否是Flutter版本问题

    构造器 FlutterView.FlutterView(Context,RenderMode)不适用 是否是Flutter版本问题

    /Users/XXXXX/AndroidGroup/mix_stack/android/src/main/java/com/yuewen/mix_stack/component/MXFlutterView.java:47: 错误: 对于FlutterView(Context,FlutterTextureView), 找不到合适的构造器 super(context, flutterTextureView); ^ 构造器 FlutterView.FlutterView(Context,RenderMode)不适用 (参数不匹配; FlutterTextureView无法转换为RenderMode) 构造器 FlutterView.FlutterView(Context,TransparencyMode)不适用 (参数不匹配; FlutterTextureView无法转换为TransparencyMode) 构造器 FlutterView.FlutterView(Context,AttributeSet)不适用 (参数不匹配; FlutterTextureView无法转换为AttributeSet)

    [✓] Flutter (Channel unknown, v1.12.13+hotfix.9, on Mac OS X 10.14 18A391, locale zh-Hans-CN) • Flutter version 1.12.13+hotfix.9 at /Users/lichenxi/flutter • Framework revision f139b11009 (10 months ago), 2020-03-30 13:57:30 -0700 • Engine revision af51afceb8 • Dart version 2.7.2

    opened by LcxAndroidwawa 1
Releases(1.4.0)
Owner
Yuewen Engineering
China Literature Limited is a pioneer of the online literature market and operates a leading online literature platform in China.
Yuewen Engineering
This is an automated library software built in Java Netbeans to reduce manual efforts of the librarian, students to ensure smooth functioning of library by involving RFIDs.

Advanced-Library-Automation-System This is an advanced automated library software built in Java Netbeans to reduce manual efforts of the librarian, st

DEV_FINWIZ 14 Dec 6, 2022
sql2o is a small library, which makes it easy to convert the result of your sql-statements into objects. No resultset hacking required. Kind of like an orm, but without the sql-generation capabilities. Supports named parameters.

sql2o Sql2o is a small java library, with the purpose of making database interaction easy. When fetching data from the database, the ResultSet will au

Lars Aaberg 1.1k Dec 28, 2022
IoTDB (Internet of Things Database) is a data management system for time series data

English | 中文 IoTDB Overview IoTDB (Internet of Things Database) is a data management system for time series data, which can provide users specific ser

The Apache Software Foundation 3k Jan 1, 2023
SceneView is a 3D/AR Android View with ARCore and Google Filament. This is the newest way to make your Android 3D/AR app.

SceneView is a 3D/AR Android View with ARCore and Google Filament This is Sceneform replacement Features Use SceneView for 3D only or ArSceneView for

SceneView Open Community 235 Jan 4, 2023
Vibur DBCP - concurrent and dynamic JDBC connection pool

Vibur DBCP is concurrent, fast, and fully-featured JDBC connection pool, which provides advanced performance monitoring capabilities, including slow S

Vibur 94 Apr 20, 2022
A RatingBar library for android, you can customize size, spacing, color and image easily, and support right to left.

AndRatingBar A RatingBar library for android, you can customize size, spacing, color and image easily, and support right to left. 安卓RatingBar终极方案,继承自原

dqq 271 Aug 14, 2021
🔥 强大的动态线程池,附带监控线程池功能(没有依赖任何中间件)。Powerful dynamic thread pool, does not rely on any middleware, with monitoring thread pool function.

ThreadPool, so easy. 动态线程池监控,主意来源于美团技术公众号 点击查看美团线程池文章 看了文章后深受感触,再加上最近线上线程池的不可控以及不可逆等问题,想做出一个兼容性、功能性、易上手等特性集于一身的的开源项目。目标还是要有的,虽然过程可能会艰辛 目前这个项目是由作者独立开发,

龙台 3.4k Jan 3, 2023
🔥 强大的动态线程池,附带监控线程池功能(没有依赖任何中间件)。Powerful dynamic thread pool, does not rely on any middleware, with monitoring thread pool function.

?? 动态线程池系统,包含 Server 端及 SpringBoot Client 端需引入的 Starter. 动态线程池监控,主意来源于美团技术公众号 点击查看美团线程池文章 看了文章后深受感触,再加上最近线上线程池的不可控以及不可逆等问题,想做出一个 兼容性、功能性、易上手等特性 集于一身的的

龙台 3.4k Jan 3, 2023
HasorDB is a Full-featured database access tool, Providing object mapping,Richer type handling than Mybatis, Dynamic SQL

HasorDB is a Full-featured database access tool, Providing object mapping,Richer type handling than Mybatis, Dynamic SQL, stored procedures, more dialect 20+, nested transactions, multiple data sources, conditional constructors, INSERT strategies, multiple statements/multiple results. And compatible with Spring and MyBatis usage.

赵永春 17 Oct 27, 2022