Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New architecture support #550

Merged
merged 16 commits into from
Jun 18, 2024
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
- https://github.com/Shopify/flash-list/pull/1076
- Fix stale reference to onScroll and onLoad
- https://github.com/Shopify/flash-list/pull/1112
- New architecture support
- https://github.com/Shopify/flash-list/pull/550

## [1.6.4] - 2024-03-18

Expand Down
15 changes: 15 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
def isNewArchitectureEnabled() {
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
}

apply plugin: 'com.android.library'

apply plugin: 'kotlin-android'

if (isNewArchitectureEnabled()) {
apply plugin: 'com.facebook.react'
}

def _ext = rootProject.ext

def _reactNativeVersion = _ext.has('reactNative') ? _ext.reactNative : '+'
Expand Down Expand Up @@ -40,11 +48,18 @@ android {
debug.java.srcDirs += 'src/debug/kotlin'
test.java.srcDirs += 'src/test/kotlin'
androidTest.java.srcDirs += 'src/androidTest/kotlin'

if (isNewArchitectureEnabled()) {
main.java.srcDirs += ['src/fabric/java']
} else {
main.java.srcDirs += ['src/paper/java']
}
}

defaultConfig {
minSdkVersion _minSdkVersion
targetSdkVersion _targetSdkVersion
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
versionCode 1
versionName "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.shopify.reactnative.flash_list

import com.facebook.react.bridge.ReactContext
import com.facebook.react.fabric.FabricUIManager
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.common.UIManagerType
import com.facebook.react.bridge.WritableMap

fun ReactContext.dispatchEvent(nativeTag: Int, eventName: String, event: WritableMap) {
val fabricUIManager = UIManagerHelper.getUIManager(this, UIManagerType.FABRIC) as FabricUIManager
fabricUIManager.receiveEvent(nativeTag, eventName, event)
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,6 @@ class AutoLayoutView(context: Context) : ReactViewGroup(context) {
event.putDouble("offsetStart", alShadow.blankOffsetAtStart / pixelDensity)
event.putDouble("offsetEnd", alShadow.blankOffsetAtEnd / pixelDensity)
val reactContext = context as ReactContext
reactContext
.getJSModule(RCTEventEmitter::class.java)
.receiveEvent(id, "onBlankAreaEvent", event)
reactContext.dispatchEvent(id, "onBlankAreaEvent", event)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ package com.shopify.reactnative.flash_list
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.views.view.ReactViewGroup
import com.facebook.react.views.view.ReactViewManager
import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.viewmanagers.AutoLayoutViewManagerDelegate
import com.facebook.react.viewmanagers.AutoLayoutViewManagerInterface
import kotlin.math.roundToInt

/** ViewManager for AutoLayoutView - Container for all RecyclerListView children. Automatically removes all gaps and overlaps for GridLayouts with flexible spans.
* Note: This cannot work for masonry layouts i.e, pinterest like layout */
@ReactModule(name = AutoLayoutViewManager.REACT_CLASS)
class AutoLayoutViewManager: ReactViewManager() {
class AutoLayoutViewManager: ViewGroupManager<AutoLayoutView>(), AutoLayoutViewManagerInterface<AutoLayoutView> {
private val mDelegate: AutoLayoutViewManagerDelegate<AutoLayoutView, AutoLayoutViewManager> =
AutoLayoutViewManagerDelegate(this)

companion object {
const val REACT_CLASS = "AutoLayoutView"
Expand All @@ -21,45 +24,44 @@ class AutoLayoutViewManager: ReactViewManager() {
return REACT_CLASS
}

override fun createViewInstance(context: ThemedReactContext): ReactViewGroup {
override fun getDelegate(): ViewManagerDelegate<AutoLayoutView> = mDelegate

override fun createViewInstance(context: ThemedReactContext): AutoLayoutView {
return AutoLayoutView(context).also { it.pixelDensity = context.resources.displayMetrics.density.toDouble() }
}

override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
return MapBuilder.builder<String, Any>().put(
"onBlankAreaEvent",
MapBuilder.of(
"registrationName", "onBlankAreaEvent")
).build();
}
override fun getExportedCustomDirectEventTypeConstants() = mutableMapOf(
"onBlankAreaEvent" to mutableMapOf("registrationName" to "onBlankAreaEvent"),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need here both onBlankAreaEvent and topOnBlankAreaEvent?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I don't remember 😅. It's been a while since I've written this, but it doesn't seem to be causing any issues when I removed topOnBlankAreaEvent.

"topOnBlankAreaEvent" to mutableMapOf("registrationName" to "onBlankAreaEvent"),
)

@ReactProp(name = "horizontal")
fun setHorizontal(view: AutoLayoutView, isHorizontal: Boolean) {
override fun setHorizontal(view: AutoLayoutView, isHorizontal: Boolean) {
view.alShadow.horizontal = isHorizontal
}

@ReactProp(name = "disableAutoLayout")
fun setDisableAutoLayout(view: AutoLayoutView, disableAutoLayout: Boolean) {
override fun setDisableAutoLayout(view: AutoLayoutView, disableAutoLayout: Boolean) {
view.disableAutoLayout = disableAutoLayout
}

@ReactProp(name = "scrollOffset")
fun setScrollOffset(view: AutoLayoutView, scrollOffset: Double) {
override fun setScrollOffset(view: AutoLayoutView, scrollOffset: Double) {
view.alShadow.scrollOffset = convertToPixelLayout(scrollOffset, view.pixelDensity)
}

@ReactProp(name = "windowSize")
fun setWindowSize(view: AutoLayoutView, windowSize: Double) {
override fun setWindowSize(view: AutoLayoutView, windowSize: Double) {
view.alShadow.windowSize = convertToPixelLayout(windowSize, view.pixelDensity)
}

@ReactProp(name = "renderAheadOffset")
fun setRenderAheadOffset(view: AutoLayoutView, renderOffset: Double) {
override fun setRenderAheadOffset(view: AutoLayoutView, renderOffset: Double) {
view.alShadow.renderOffset = convertToPixelLayout(renderOffset, view.pixelDensity)
}

@ReactProp(name = "enableInstrumentation")
fun setEnableInstrumentation(view: AutoLayoutView, enableInstrumentation: Boolean) {
override fun setEnableInstrumentation(view: AutoLayoutView, enableInstrumentation: Boolean) {
view.enableInstrumentation = enableInstrumentation
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ package com.shopify.reactnative.flash_list

import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.views.view.ReactViewGroup
import com.facebook.react.views.view.ReactViewManager
import com.facebook.react.viewmanagers.CellContainerManagerDelegate
import com.facebook.react.viewmanagers.CellContainerManagerInterface

@ReactModule(name = AutoLayoutViewManager.REACT_CLASS)
class CellContainerManager: ReactViewManager() {
class CellContainerManager: ViewGroupManager<CellContainerImpl>(), CellContainerManagerInterface<CellContainerImpl> {
private val mDelegate: CellContainerManagerDelegate<CellContainerImpl, CellContainerManager>
= CellContainerManagerDelegate(this)

companion object {
const val REACT_CLASS = "CellContainer"
}
Expand All @@ -16,12 +21,14 @@ class CellContainerManager: ReactViewManager() {
return REACT_CLASS
}

override fun createViewInstance(context: ThemedReactContext): ReactViewGroup {
override fun getDelegate(): ViewManagerDelegate<CellContainerImpl> = mDelegate

override fun createViewInstance(context: ThemedReactContext): CellContainerImpl {
return CellContainerImpl(context)
}

@ReactProp(name = "index")
fun setIndex(view: CellContainerImpl, index: Int) {
override fun setIndex(view: CellContainerImpl, index: Int) {
view.index = index
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the generator always creating code for paper as well? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's not. I copied those files without modifying them. Do you think those comments should be removed?

*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
* @generated by codegen project: GeneratePropsJavaDelegate.js
*/

package com.facebook.react.viewmanagers;

import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.uimanager.BaseViewManagerDelegate;
import com.facebook.react.uimanager.BaseViewManagerInterface;

public class AutoLayoutViewManagerDelegate<T extends View, U extends BaseViewManagerInterface<T> & AutoLayoutViewManagerInterface<T>> extends BaseViewManagerDelegate<T, U> {
public AutoLayoutViewManagerDelegate(U viewManager) {
super(viewManager);
}
@Override
public void setProperty(T view, String propName, @Nullable Object value) {
switch (propName) {
case "horizontal":
mViewManager.setHorizontal(view, value == null ? false : (boolean) value);
break;
case "scrollOffset":
mViewManager.setScrollOffset(view, value == null ? 0f : ((Double) value).doubleValue());
break;
case "windowSize":
mViewManager.setWindowSize(view, value == null ? 0f : ((Double) value).doubleValue());
break;
case "renderAheadOffset":
mViewManager.setRenderAheadOffset(view, value == null ? 0f : ((Double) value).doubleValue());
break;
case "enableInstrumentation":
mViewManager.setEnableInstrumentation(view, value == null ? false : (boolean) value);
break;
case "disableAutoLayout":
mViewManager.setDisableAutoLayout(view, value == null ? false : (boolean) value);
break;
default:
super.setProperty(view, propName, value);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
* @generated by codegen project: GeneratePropsJavaInterface.js
*/

package com.facebook.react.viewmanagers;

import android.view.View;

public interface AutoLayoutViewManagerInterface<T extends View> {
void setHorizontal(T view, boolean value);
void setScrollOffset(T view, double value);
void setWindowSize(T view, double value);
void setRenderAheadOffset(T view, double value);
void setEnableInstrumentation(T view, boolean value);
void setDisableAutoLayout(T view, boolean value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
* @generated by codegen project: GeneratePropsJavaDelegate.js
*/

package com.facebook.react.viewmanagers;

import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.uimanager.BaseViewManagerDelegate;
import com.facebook.react.uimanager.BaseViewManagerInterface;

public class CellContainerManagerDelegate<T extends View, U extends BaseViewManagerInterface<T> & CellContainerManagerInterface<T>> extends BaseViewManagerDelegate<T, U> {
public CellContainerManagerDelegate(U viewManager) {
super(viewManager);
}
@Override
public void setProperty(T view, String propName, @Nullable Object value) {
switch (propName) {
case "index":
mViewManager.setIndex(view, value == null ? 0 : ((Double) value).intValue());
break;
default:
super.setProperty(view, propName, value);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
* @generated by codegen project: GeneratePropsJavaInterface.js
*/

package com.facebook.react.viewmanagers;

import android.view.View;

public interface CellContainerManagerInterface<T extends View> {
void setIndex(T view, int value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

package com.shopify.reactnative.flash_list

import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.events.RCTEventEmitter
import com.facebook.react.bridge.WritableMap

fun ReactContext.dispatchEvent(nativeTag: Int, eventName: String, event: WritableMap) {
this.getJSModule(RCTEventEmitter::class.java)
.receiveEvent(nativeTag, eventName, event)
}
19 changes: 17 additions & 2 deletions ios/Sources/AutoLayoutView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import UIKit
/// Container for all RecyclerListView children. This will automatically remove all gaps and overlaps for GridLayouts with flexible spans.
/// Note: This cannot work for masonry layouts i.e, pinterest like layout
@objc public class AutoLayoutView: UIView {
#if RCT_NEW_ARCH_ENABLED
@objc public var onBlankAreaEventHandler: ((CGFloat, CGFloat) -> Void)?
#endif

@objc(onBlankAreaEvent)
var onBlankAreaEvent: RCTDirectEventBlock?

Expand Down Expand Up @@ -68,13 +72,17 @@ import UIKit
distanceFromWindowStart: distanceFromWindowStart,
distanceFromWindowEnd: distanceFromWindowEnd
)


#if RCT_NEW_ARCH_ENABLED
onBlankAreaEventHandler?(blankOffsetStart, blankOffsetEnd)
#else
onBlankAreaEvent?(
[
"offsetStart": blankOffsetStart,
"offsetEnd": blankOffsetEnd,
]
)
#endif
}

func getScrollView() -> UIScrollView? {
Expand Down Expand Up @@ -274,6 +282,13 @@ import UIKit
}

private func footer() -> UIView? {
return superview?.subviews.first(where:{($0 as? CellContainerComponentView)?.index == -1})
// On the new arch, AutoLayoutView is wrapped with AutoLayoutViewComponentView, so we need to go up one more level
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why do we need the extra view?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the new architecture a lot of the code needed by views is implemented inside RCTViewComponentView which is written in ObjC++. Ideally to create a new view component on the new architecture, one would inherit after it but it has two problems:

  • it's not implemented on the old architecture, and would require some tricks to make it work
  • I don't think it's possible for a Swift class to inherit after ObjC++ class

Instead of that, it's possible to create a wrapper view that inherits after RCTViewComponentView (in this case AutoLayoutViewComponentView) that will handle props and events of the underlying view that's already been implemented.

#if RCT_NEW_ARCH_ENABLED
let parentSubviews = superview?.superview?.subviews
#else
let parentSubviews = superview?.subviews
#endif

return parentSubviews?.first(where:{($0 as? CellContainerComponentView)?.index == -1})
}
}
Loading
Loading