001package arez.component.internal; 002 003import arez.ActionFlags; 004import arez.Arez; 005import arez.ArezContext; 006import arez.Component; 007import arez.ComputableValue; 008import arez.Disposable; 009import arez.ObservableValue; 010import arez.Procedure; 011import arez.SafeProcedure; 012import arez.Task; 013import arez.annotations.ArezComponent; 014import arez.annotations.CascadeDispose; 015import arez.annotations.Observe; 016import arez.component.ComponentObservable; 017import grim.annotations.OmitClinit; 018import grim.annotations.OmitSymbol; 019import java.util.ArrayList; 020import java.util.HashMap; 021import java.util.Map; 022import java.util.Objects; 023import javax.annotation.Nonnull; 024import javax.annotation.Nullable; 025import static org.realityforge.braincheck.Guards.*; 026 027/** 028 * The "kernel" of the components generated by the annotation processor. 029 * This class exists so that code common across multiple components is not present in every 030 * generated class but is instead in a single location. This results in smaller, faster code. 031 */ 032@OmitClinit 033public final class ComponentKernel 034 implements Disposable, ComponentObservable 035{ 036 /** 037 * The component has been created, but not yet initialized. 038 */ 039 private final static byte COMPONENT_CREATED = 0; 040 /** 041 * The components constructor has been called, the {@link ArezContext} field initialized (if necessary), 042 * and the synthetic id has been generated (if required). 043 */ 044 private final static byte COMPONENT_INITIALIZED = 1; 045 /** 046 * The reactive elements have been created (i.e. the {@link ObservableValue}, {@link arez.Observer}, 047 * {@link ComputableValue} etc.). The {@link arez.annotations.PostConstruct} has NOT been invoked nor 048 * has the {@link Component} been instantiated. This means the component is ready to be interacted with 049 * in a {@link arez.annotations.PostConstruct} method but has not been fully constructed. 050 */ 051 private final static byte COMPONENT_CONSTRUCTED = 2; 052 /** 053 * The {@link arez.annotations.PostConstruct} method has been invoked and 054 * the {@link Component} has been instantiated. Observers have been scheduled but the scheduler 055 * has not been triggered. 056 */ 057 private final static byte COMPONENT_COMPLETE = 3; 058 /** 059 * The scheduler has been triggered and any {@link Observe} methods have been invoked if runtime managed. 060 */ 061 private final static byte COMPONENT_READY = 4; 062 /** 063 * The component is disposing. 064 */ 065 private final static byte COMPONENT_DISPOSING = -2; 066 /** 067 * The component has been disposed. 068 */ 069 private final static byte COMPONENT_DISPOSED = -1; 070 /** 071 * Reference to the context to which this component belongs. 072 */ 073 @OmitSymbol( unless = "arez.enable_zones" ) 074 @Nullable 075 private final ArezContext _context; 076 /** 077 * A human consumable name for component. It should be non-null if {@link Arez#areNamesEnabled()} returns 078 * <code>true</code> and <code>null</code> otherwise. 079 */ 080 @Nullable 081 @OmitSymbol( unless = "arez.enable_names" ) 082 private final String _name; 083 /** 084 * The runtime managed synthetic id for component. This will be 0 if the component has supplied a custom 085 * id via a method annotated with {@link arez.annotations.ComponentId} or the annotation processor has 086 * determined that no id is required. The id must be supplied with a non-zero value if: 087 * 088 * <ul> 089 * <li>the component declared it requires an id (i.e. {@link ArezComponent#requireId()} is <code>true</code>) but 090 * no method annotated with {@link arez.annotations.ComponentId} is present on the components type.</li> 091 * <li>The runtime requires an id as part of debugging infrastructure. (i.e. @link Arez#areNamesEnabled(), {@link Arez#areRegistriesEnabled()} 092 * or {@link Arez#areNativeComponentsEnabled()} returns <code>true</code>.</li> 093 * </ul> 094 */ 095 private final int _id; 096 /** 097 * The initialization state of the component. Possible values are defined by the constants in the 098 * this class however this field is only used for determining whether a component 099 * is disposed when invariant checking is disabled so states other than {@link #COMPONENT_DISPOSING} are not set 100 * when invariant checking is disabled. 101 */ 102 private byte _state; 103 /** 104 * The native component associated with the component. This should be non-null if {@link Arez#areNativeComponentsEnabled()} 105 * returns <code>true</code> and <code>null</code> otherwise. 106 */ 107 @OmitSymbol( unless = "arez.enable_native_components" ) 108 @Nullable 109 private final Component _component; 110 /** 111 * This callback is invoked before the component is disposed. 112 */ 113 @Nullable 114 private final SafeProcedure _preDisposeCallback; 115 /** 116 * This callback is invoked to dispose the reactive elements of the component. 117 */ 118 @Nullable 119 private final SafeProcedure _disposeCallback; 120 /** 121 * This callback is invoked after the component is disposed. 122 */ 123 @Nullable 124 private final SafeProcedure _postDisposeCallback; 125 /** 126 * The mechanisms to notify downstream elements that the component has been disposed. This should be non-null 127 * if the {@link ArezComponent#disposeNotifier()} is enabled, and <code>null</code> otherwise. 128 */ 129 @Nullable 130 private final Map<Object, SafeProcedure> _onDisposeListeners; 131 /** 132 * Mechanism for implementing {@link ComponentObservable} on the component. 133 */ 134 @Nullable 135 private final ObservableValue<Boolean> _componentObservable; 136 /** 137 * Flag that indicates whether {@link ArezComponent#disposeOnDeactivate()} is true for component. 138 */ 139 private final boolean _disposeOnDeactivate; 140 /** 141 * Mechanism for implementing {@link ArezComponent#disposeOnDeactivate()} on the component. 142 */ 143 @Nullable 144 private final ComputableValue<Boolean> _disposeOnDeactivateValue; 145 /** 146 * Guard to ensure we never try to schedule a dispose multiple times, otherwise the underlying task 147 * system will detect multiple tasks with the same name and object. 148 */ 149 private boolean _disposeScheduled; 150 151 public ComponentKernel( @Nullable final ArezContext context, 152 @Nullable final String name, 153 final int id, 154 @Nullable final Component component, 155 @Nullable final SafeProcedure preDisposeCallback, 156 @Nullable final SafeProcedure disposeCallback, 157 @Nullable final SafeProcedure postDisposeCallback, 158 final boolean notifyOnDispose, 159 final boolean isComponentObservable, 160 final boolean disposeOnDeactivate ) 161 { 162 if ( Arez.shouldCheckApiInvariants() ) 163 { 164 apiInvariant( () -> Arez.areZonesEnabled() || null == context, 165 () -> "Arez-0100: ComponentKernel passed a context but Arez.areZonesEnabled() is false" ); 166 apiInvariant( () -> Arez.areNamesEnabled() || null == name, 167 () -> "Arez-0156: ComponentKernel passed a name '" + name + 168 "' but Arez.areNamesEnabled() returns false." ); 169 apiInvariant( () -> !Arez.areNativeComponentsEnabled() || 170 null == component || 171 0 == id || 172 ( (Integer) id ).equals( component.getId() ), 173 () -> "Arez-0222: ComponentKernel named '" + name + 174 "' passed an id " + id + " and a component but the component had a different id (" + 175 Objects.requireNonNull( component ).getId() + ")" ); 176 } 177 178 if ( Arez.shouldCheckApiInvariants() ) 179 { 180 _state = COMPONENT_INITIALIZED; 181 } 182 _name = Arez.areNamesEnabled() ? name : null; 183 _context = Arez.areZonesEnabled() ? context : null; 184 _component = Arez.areNativeComponentsEnabled() ? Objects.requireNonNull( component ) : null; 185 _id = id; 186 _onDisposeListeners = notifyOnDispose ? new HashMap<>() : null; 187 _preDisposeCallback = Arez.areNativeComponentsEnabled() ? null : preDisposeCallback; 188 _disposeCallback = Arez.areNativeComponentsEnabled() ? null : disposeCallback; 189 _postDisposeCallback = Arez.areNativeComponentsEnabled() ? null : postDisposeCallback; 190 _componentObservable = isComponentObservable ? createComponentObservable() : null; 191 _disposeOnDeactivate = disposeOnDeactivate; 192 _disposeOnDeactivateValue = disposeOnDeactivate ? createDisposeOnDeactivate() : null; 193 } 194 195 @Nonnull 196 private ComputableValue<Boolean> createDisposeOnDeactivate() 197 { 198 return getContext().computable( Arez.areNativeComponentsEnabled() ? getComponent() : null, 199 Arez.areNamesEnabled() ? getName() + ".disposeOnDeactivate" : null, 200 this::observe0, 201 ComputableValue.Flags.PRIORITY_HIGHEST ); 202 } 203 204 private void scheduleDispose() 205 { 206 /* 207 * Guard against a scenario where due to interleaving of scheduled tasks a component is disposed due, 208 * to deactivation and then is re-observed and deactivated again prior to the dispose task running. 209 * This scenario was thought to be practically impossible but several applications did the impossible. 210 * 211 * There is still a bug or at least an ambiguity where a disposeOnDeactivate component deactivates, schedules 212 * dispose and then activates before the dispose task runs. Should the dispose be aborted or should it go ahead? 213 * Currently the Arez API does not expose a flag indicating whether computableValues are observed and not possible 214 * to implement the first strategy even though it may seem to be the right one. 215 */ 216 if ( !_disposeScheduled ) 217 { 218 _disposeScheduled = true; 219 getContext().task( Arez.areNamesEnabled() ? getName() + ".disposeOnDeactivate.task" : null, 220 this::dispose, 221 Task.Flags.PRIORITY_HIGHEST | Task.Flags.DISPOSE_ON_COMPLETE | Task.Flags.NO_WRAP_TASK ); 222 } 223 } 224 225 @Nonnull 226 private ObservableValue<Boolean> createComponentObservable() 227 { 228 return getContext().observable( Arez.areNativeComponentsEnabled() ? getComponent() : null, 229 Arez.areNamesEnabled() ? getName() + ".isDisposed" : null, 230 Arez.arePropertyIntrospectorsEnabled() ? () -> _state > 0 : null ); 231 } 232 233 @Override 234 public boolean observe() 235 { 236 if ( Arez.shouldCheckApiInvariants() ) 237 { 238 apiInvariant( () -> null != _disposeOnDeactivateValue || null != _componentObservable, 239 () -> "Arez-0221: ComponentKernel.observe() invoked on component named '" + getName() + 240 "' but observing is not enabled for component." ); 241 } 242 if ( null != _disposeOnDeactivateValue ) 243 { 244 return isNotDisposed() ? _disposeOnDeactivateValue.get() : false; 245 } 246 else 247 { 248 return observe0(); 249 } 250 } 251 252 /** 253 * Internal observe method that may be directly used or used from computable if disposeOnDeactivate is true. 254 */ 255 private boolean observe0() 256 { 257 assert null != _componentObservable; 258 if ( _disposeOnDeactivate ) 259 { 260 getContext().registerHook( "$DOD$", null, this::scheduleDispose ); 261 } 262 final boolean isNotDisposed = isNotDisposed(); 263 if ( isNotDisposed ) 264 { 265 _componentObservable.reportObserved(); 266 } 267 268 return isNotDisposed; 269 } 270 271 @Override 272 public void dispose() 273 { 274 if ( isNotDisposed() ) 275 { 276 // Note that his state transition occurs outside the guard as it is required to compute isDisposed() state 277 _state = COMPONENT_DISPOSING; 278 if ( Arez.areNativeComponentsEnabled() ) 279 { 280 assert null != _component; 281 _component.dispose(); 282 } 283 else 284 { 285 getContext().safeAction( Arez.areNamesEnabled() ? getName() + ".dispose" : null, 286 this::performDispose, 287 ActionFlags.NO_VERIFY_ACTION_REQUIRED ); 288 } 289 if ( Arez.shouldCheckApiInvariants() ) 290 { 291 _state = COMPONENT_DISPOSED; 292 } 293 } 294 } 295 296 private void performDispose() 297 { 298 invokeCallbackIfNecessary( _preDisposeCallback ); 299 releaseResources(); 300 invokeCallbackIfNecessary( _disposeCallback ); 301 invokeCallbackIfNecessary( _postDisposeCallback ); 302 } 303 304 private void invokeCallbackIfNecessary( @Nullable final SafeProcedure callback ) 305 { 306 if ( null != callback ) 307 { 308 callback.call(); 309 } 310 } 311 312 @Override 313 public boolean isDisposed() 314 { 315 return _state < 0; 316 } 317 318 private void releaseResources() 319 { 320 if ( null != _onDisposeListeners ) 321 { 322 notifyOnDisposeListeners(); 323 } 324 // If native components are enabled, these elements are registered with native component 325 // and will thus be disposed as part 326 if ( !Arez.areNativeComponentsEnabled() ) 327 { 328 Disposable.dispose( _componentObservable ); 329 Disposable.dispose( _disposeOnDeactivateValue ); 330 } 331 } 332 333 /** 334 * Notify an OnDispose listeners that have been added to the component. 335 * This method MUST only be called if the component has enabled onDisposeNotify feature. 336 */ 337 public void notifyOnDisposeListeners() 338 { 339 assert null != _onDisposeListeners; 340 for ( final Map.Entry<Object, SafeProcedure> entry : new ArrayList<>( _onDisposeListeners.entrySet() ) ) 341 { 342 final Object key = entry.getKey(); 343 /* 344 * There is scenarios where there is multiple elements being simultaneously disposed and 345 * the @CascadeDispose has not triggered so a disposed object is in this list waiting to 346 * be called back. If the callback is triggered and the @CascadeDispose is on an observable 347 * property then the framework will attempt to null field and generate invariant failures 348 * or runtime errors unless we skip the callback and just remove the listener. 349 */ 350 if ( !Disposable.isDisposed( key ) ) 351 { 352 entry.getValue().call(); 353 } 354 } 355 } 356 357 /** 358 * Return true if the component has been initialized. 359 * 360 * @return true if the component has been initialized. 361 */ 362 public boolean hasBeenInitialized() 363 { 364 assert Arez.shouldCheckInvariants() || Arez.shouldCheckApiInvariants(); 365 return COMPONENT_CREATED != _state; 366 } 367 368 /** 369 * Return true if the component has been constructed. 370 * 371 * @return true if the component has been constructed. 372 */ 373 public boolean hasBeenConstructed() 374 { 375 assert Arez.shouldCheckInvariants() || Arez.shouldCheckApiInvariants(); 376 return hasBeenInitialized() && COMPONENT_INITIALIZED != _state; 377 } 378 379 /** 380 * Return true if the component has been completed. 381 * 382 * @return true if the component has been completed. 383 */ 384 public boolean hasBeenCompleted() 385 { 386 assert Arez.shouldCheckInvariants() || Arez.shouldCheckApiInvariants(); 387 return hasBeenConstructed() && COMPONENT_CONSTRUCTED != _state; 388 } 389 390 /** 391 * Return true if the component is in COMPONENT_CONSTRUCTED state. 392 * 393 * @return true if the component is in COMPONENT_CONSTRUCTED state. 394 */ 395 public boolean isConstructed() 396 { 397 return COMPONENT_CONSTRUCTED == _state; 398 } 399 400 /** 401 * Return true if the component is in COMPONENT_COMPLETE state. 402 * 403 * @return true if the component is in COMPONENT_COMPLETE state. 404 */ 405 public boolean isComplete() 406 { 407 return COMPONENT_COMPLETE == _state; 408 } 409 410 /** 411 * Return true if the component is ready. 412 * 413 * @return true if the component is ready. 414 */ 415 public boolean isReady() 416 { 417 return COMPONENT_READY == _state; 418 } 419 420 /** 421 * Return true if the component is NOT ready. 422 * 423 * @return true if the component is NOT ready. 424 */ 425 public boolean isNotReady() 426 { 427 return !isReady(); 428 } 429 430 /** 431 * Return true if the component is disposing. 432 * 433 * @return true if the component is disposing. 434 */ 435 public boolean isDisposing() 436 { 437 return COMPONENT_DISPOSING == _state; 438 } 439 440 /** 441 * Return true if the component is active and can be interacted with. 442 * This means that the component has been constructed and has not started to be disposed. 443 * 444 * @return true if the component is active. 445 */ 446 public boolean isActive() 447 { 448 assert Arez.shouldCheckInvariants() || Arez.shouldCheckApiInvariants(); 449 return COMPONENT_CONSTRUCTED == _state || COMPONENT_COMPLETE == _state || COMPONENT_READY == _state; 450 } 451 452 /** 453 * Describe component state. This is usually used to provide error messages. 454 * 455 * @return a string description of the state. 456 */ 457 @Nonnull 458 public String describeState() 459 { 460 return describeState( _state ); 461 } 462 463 @Nonnull 464 private String describeState( final int state ) 465 { 466 assert Arez.shouldCheckInvariants() || Arez.shouldCheckApiInvariants(); 467 switch ( state ) 468 { 469 case ComponentKernel.COMPONENT_CREATED: 470 return "created"; 471 case ComponentKernel.COMPONENT_INITIALIZED: 472 return "initialized"; 473 case ComponentKernel.COMPONENT_CONSTRUCTED: 474 return "constructed"; 475 case ComponentKernel.COMPONENT_COMPLETE: 476 return "complete"; 477 case ComponentKernel.COMPONENT_READY: 478 return "ready"; 479 case ComponentKernel.COMPONENT_DISPOSING: 480 return "disposing"; 481 default: 482 assert ComponentKernel.COMPONENT_DISPOSED == state; 483 return "disposed"; 484 } 485 } 486 487 /** 488 * Transition component state from {@link ComponentKernel#COMPONENT_INITIALIZED} to {@link ComponentKernel#COMPONENT_CONSTRUCTED}. 489 */ 490 public void componentConstructed() 491 { 492 if ( Arez.shouldCheckApiInvariants() ) 493 { 494 apiInvariant( () -> COMPONENT_INITIALIZED == _state, 495 () -> "Arez-0219: Bad state transition from " + describeState( _state ) + 496 " to " + describeState( COMPONENT_CONSTRUCTED ) + 497 " on component named '" + getName() + "'." ); 498 _state = COMPONENT_CONSTRUCTED; 499 } 500 } 501 502 /** 503 * Transition component state from {@link ComponentKernel#COMPONENT_INITIALIZED} to 504 * {@link ComponentKernel#COMPONENT_CONSTRUCTED} and then to {@link ComponentKernel#COMPONENT_READY}. 505 * This should only be called if there is active elements that are part of the component that need to be scheduled, 506 * otherwise the component can transition directly to ready. 507 */ 508 public void componentComplete() 509 { 510 completeNativeComponent(); 511 if ( Arez.shouldCheckApiInvariants() ) 512 { 513 apiInvariant( () -> COMPONENT_CONSTRUCTED == _state, 514 () -> "Arez-0220: Bad state transition from " + describeState( _state ) + 515 " to " + describeState( COMPONENT_COMPLETE ) + 516 " on component named '" + getName() + "'." ); 517 _state = COMPONENT_COMPLETE; 518 } 519 // Trigger scheduler so active parts of components can react 520 getContext().triggerScheduler(); 521 makeComponentReady(); 522 } 523 524 /** 525 * Transition component state from {@link ComponentKernel#COMPONENT_CONSTRUCTED} to {@link ComponentKernel#COMPONENT_READY}. 526 * This should be invoked rather than {@link #componentComplete()} if there is no active elements of the component that 527 * need to be scheduled. 528 */ 529 public void componentReady() 530 { 531 completeNativeComponent(); 532 makeComponentReady(); 533 } 534 535 /** 536 * Mark the native component if present as complete. 537 */ 538 private void completeNativeComponent() 539 { 540 if ( Arez.areNativeComponentsEnabled() ) 541 { 542 assert null != _component; 543 _component.complete(); 544 } 545 } 546 547 private void makeComponentReady() 548 { 549 if ( Arez.shouldCheckApiInvariants() ) 550 { 551 apiInvariant( () -> COMPONENT_CONSTRUCTED == _state || COMPONENT_COMPLETE == _state, 552 () -> "Arez-0218: Bad state transition from " + describeState( _state ) + 553 " to " + describeState( COMPONENT_READY ) + 554 " on component named '" + getName() + "'." ); 555 _state = COMPONENT_READY; 556 } 557 } 558 559 /** 560 * Return the context in which this component was created. 561 * 562 * @return the associated context. 563 */ 564 @Nonnull 565 public ArezContext getContext() 566 { 567 return Arez.areZonesEnabled() ? Objects.requireNonNull( _context ) : Arez.context(); 568 } 569 570 /** 571 * Invoke the setter in a transaction. 572 * If a transaction is active then invoke the setter directly, otherwise wrap the setter in an action. 573 * 574 * @param name the name of the action if it is needed. 575 * @param setter the setter action to invoke. 576 */ 577 public void safeSetObservable( @Nullable final String name, @Nonnull final SafeProcedure setter ) 578 { 579 if ( getContext().isTransactionActive() ) 580 { 581 setter.call(); 582 } 583 else 584 { 585 getContext().safeAction( Arez.areNamesEnabled() ? name : null, setter ); 586 } 587 } 588 589 /** 590 * Invoke the setter in a transaction. 591 * If a transaction is active then invoke the setter directly, otherwise wrap the setter in an action. 592 * 593 * @param name the name of the action if it is needed. 594 * @param setter the setter action to invoke. 595 * @throws Throwable if setter throws an exception. 596 */ 597 public void setObservable( @Nullable final String name, @Nonnull final Procedure setter ) 598 throws Throwable 599 { 600 if ( getContext().isTransactionActive() ) 601 { 602 setter.call(); 603 } 604 else 605 { 606 getContext().action( Arez.areNamesEnabled() ? name : null, setter ); 607 } 608 } 609 610 /** 611 * Return the name of the component. 612 * This method should NOT be invoked unless {@link Arez#areNamesEnabled()} returns true and will throw an 613 * exception if invariant checking is enabled. 614 * 615 * @return the name of the component. 616 */ 617 @Nonnull 618 public String getName() 619 { 620 if ( Arez.shouldCheckApiInvariants() ) 621 { 622 apiInvariant( Arez::areNamesEnabled, 623 () -> "Arez-0164: ComponentKernel.getName() invoked when Arez.areNamesEnabled() returns false." ); 624 } 625 assert null != _name; 626 return _name; 627 } 628 629 /** 630 * Return the synthetic id associated with the component. 631 * This method MUST NOT be invoked if a synthetic id is not present and will generate an invariant failure 632 * when invariants are enabled. 633 * 634 * @return the synthetic id associated with the component. 635 */ 636 public int getId() 637 { 638 if ( Arez.shouldCheckApiInvariants() ) 639 { 640 apiInvariant( () -> 0 != _id, 641 () -> "Arez-0213: Attempted to unexpectedly invoke ComponentKernel.getId() method to access " + 642 "synthetic id on component named '" + getName() + "'." ); 643 } 644 return _id; 645 } 646 647 /** 648 * Return the native component associated with the component. 649 * This method MUST NOT be invoked if native components are disabled. 650 * 651 * @return the native component associated with the component. 652 */ 653 @OmitSymbol( unless = "arez.enable_native_components" ) 654 @Nonnull 655 public Component getComponent() 656 { 657 if ( Arez.shouldCheckApiInvariants() ) 658 { 659 apiInvariant( () -> null != _component, 660 () -> "Arez-0216: ComponentKernel.getComponent() invoked when Arez.areNativeComponentsEnabled() " + 661 "returns false on component named '" + getName() + "'." ); 662 } 663 assert null != _component; 664 return _component; 665 } 666 667 /** 668 * Add the listener to notify list under key. 669 * This method MUST NOT be invoked after {@link #dispose()} has been invoked. 670 * This method should not be invoked if another listener has been added with the same key without 671 * being removed. 672 * 673 * <p>If the key implements {@link Disposable} and {@link Disposable#isDisposed()} returns <code>true</code> 674 * when invoking the calback then the callback will be skipped. This rare situation only occurs when there is 675 * circular dependency in the object model usually involving {@link CascadeDispose}.</p> 676 * 677 * @param key the key to uniquely identify listener. 678 * @param action the listener callback. 679 */ 680 public void addOnDisposeListener( @Nonnull final Object key, @Nonnull final SafeProcedure action ) 681 { 682 assert null != _onDisposeListeners; 683 if ( Arez.shouldCheckApiInvariants() ) 684 { 685 invariant( this::isNotDisposed, 686 () -> "Arez-0170: Attempting to add OnDispose listener but ComponentKernel has been disposed." ); 687 invariant( () -> !_onDisposeListeners.containsKey( key ), 688 () -> "Arez-0166: Attempting to add OnDispose listener with key '" + key + 689 "' but a listener with that key already exists." ); 690 } 691 _onDisposeListeners.put( key, action ); 692 } 693 694 /** 695 * Remove the listener with specified key from the notify list. 696 * This method should only be invoked when a listener has been added for specific key using 697 * {@link #addOnDisposeListener(Object, SafeProcedure)} and has not been removed by another 698 * call to this method. 699 * 700 * @param key the key under which the listener was previously added. 701 */ 702 public void removeOnDisposeListener( @Nonnull final Object key ) 703 { 704 assert null != _onDisposeListeners; 705 // This method can be called when the notifier is disposed to avoid the caller (i.e. per-component 706 // generated code) from checking the disposed state of the notifier before invoking this method. 707 // This is necessary in a few rare circumstances but requiring the caller to check before invocation 708 // increases the generated code size. 709 final SafeProcedure removed = _onDisposeListeners.remove( key ); 710 if ( Arez.shouldCheckApiInvariants() ) 711 { 712 invariant( () -> null != removed, 713 () -> "Arez-0167: Attempting to remove OnDispose listener with key '" + key + 714 "' but no such listener exists." ); 715 } 716 } 717 718 boolean hasOnDisposeListeners() 719 { 720 return null != _onDisposeListeners; 721 } 722 723 @Nonnull 724 Map<Object, SafeProcedure> getOnDisposeListeners() 725 { 726 assert null != _onDisposeListeners; 727 return _onDisposeListeners; 728 } 729 730 @Nonnull 731 @Override 732 public String toString() 733 { 734 if ( Arez.areNamesEnabled() ) 735 { 736 return getName(); 737 } 738 else 739 { 740 return super.toString(); 741 } 742 } 743}