0

I'm not sure where is the bug, maybe I'm using rxjs in a wrong way. ngDestroy is not working to unsubscribe observables in NativeScript if you want to close and back to your app. I tried to work with takeUntil, but with the same results. If the user close/open the app many times, it can cause a memory leak (if I understand the mobile environment correctly). Any ideas? This code below it's only a demo. I need to use users$ in many places in my app.

Tested with Android sdk emulator and on real device.

AppComponent

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription, Observable } from 'rxjs';
import { AppService } from './app.service';

import { AuthenticationService } from './authentication.service';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnDestroy, OnInit {
  public user$: Observable<any>;

  private subscriptions: Subscription[] = [];

  constructor(private appService: AppService, private authenticationService: AuthenticationService) {}

  public ngOnInit(): void {
    this.user$ = this.authenticationService.user$;

    this.subscriptions.push(
      this.authenticationService.user$.subscribe((user: any) => {
        console.log('user', !!user);
      })
    );
  }

  public ngOnDestroy(): void {
    if (this.subscriptions) {
      this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
    }
  }

  async signIn() {
    await this.appService.signIn();
  }

  async signOut() {
    await this.appService.signOut();
  }
}

AuthenticationService

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
import { AppService } from './app.service';

@Injectable({
  providedIn: 'root',
})
export class AuthenticationService {
  public user$: Observable<any>;

  constructor(private appService: AppService) {
    this.user$ = this.appService.authState().pipe(shareReplay(1)); // I'm using this.users$ in many places in my app, so I need to use sharereplay
  }
}

AppService

import { Injectable, NgZone } from '@angular/core';
import { addAuthStateListener, login, LoginType, logout, User } from 'nativescript-plugin-firebase';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';

const user$ = new BehaviorSubject<User>(null);

@Injectable({
  providedIn: 'root',
})
export class AppService {
  constructor(private ngZone: NgZone) {
    addAuthStateListener({
      onAuthStateChanged: ({ user }) => {
        this.ngZone.run(() => {
          user$.next(user);
        });
      },
    });
  }

  public authState(): Observable<User> {
    return user$.asObservable().pipe(distinctUntilChanged());
  }

  async signIn() {
    return await login({ type: LoginType.PASSWORD, passwordOptions: { email: 'xxx', password: 'xxx' } }).catch(
      (error: string) => {
        throw {
          message: error,
        };
      }
    );
  }

  signOut() {
    logout();
  }
}
Przemo
  • 477
  • 1
  • 7
  • 16

2 Answers2

2

ngOnDestroy is called whenever a component is destroyed (following regular Angular workflow). If you have navigated forward in your app, previous views would still exist and would be unlikely to be destroyed.

If you are seeing multiple ngOnInit without any ngOnDestroy, then you have instantiated multiple components through some navigation, unrelated to your subscriptions. You should not expect the same instance of your component to be reused once ngOnDestroy has been called, so having a push to a Subscription[] array will only ever have one object.

If you are terminating the app (i.e. force quit swipe away), the whole JavaScript context is thrown out and memory is cleaned up. You won't run the risk of leaking outside of your app's context.

Incidentally, you're complicating your subscription tracking (and not just in the way that I described above about only ever having one pushed). A Subscription is an object that can have other Subscription objects attached for termination at the same time.

const subscription: Subscription = new Subscription();
subscription.add(interval(100).subscribe((n: number) => console.log(`first sub`));
subscription.add(interval(200).subscribe((n: number) => console.log(`second sub`));
subscription.add(interval(300).subscribe((n: number) => console.log(`third sub`));

timer(5000).subscribe(() => subscription.unsubscribe()); // terminates all added subscriptions

Be careful to add the subscribe call directly in .add and not with a closure. Annoyingly, this is exactly the same function call to make when you want to add a completion block to your subscription, passing a block instead:

subscription.add(() => console.log(`everybody's done.`));
Ian MacDonald
  • 13,472
  • 2
  • 30
  • 51
  • Well, I'm not talking about terminating the whole app, but about moving that app to the background of my mobile device. After logout user subscription emits as many times as I move app to the background and back before. So if I move app to the background and open that app 5 times, then if I logout my user, then this.authenticationService.user$ (tap or subscribe) emits 5 console.logs :/ I suppose it might be a problem with rxjs, but can't find the problem :/ – Przemo Apr 17 '20 at 10:48
  • I don't see anything that would cause that in the code you've posted. If you could post a [playground](https://play.nativescript.org), I might be able to get to the bottom of what you're seeing. – Ian MacDonald Apr 17 '20 at 11:17
  • I tried, but play.nativescript.com doesn't allow me to install nativescript-plugin-firebase plugin ("Plugins which have native dependencies are not supported in the Playground."). I tried to mock simple app in that tool without firebase, but I can't imitate local behaviors (for example ngOnInit is called only first time when I open demo app on my device. Locally ngOnInit is called each time I'm back to my app from the backend). Anyway, thx for help! – Przemo Apr 17 '20 at 12:17
  • I think the bug is not related to rxjs. Don't know why, but locally each time when I open/close (I mean using home button in phone device) even applicationOn is called +1 more time. Do you have any idea? I created bug here with more details: https://github.com/NativeScript/nativescript-angular/issues/2162 it's weird but on https://play.nativescript.org/ this bug not happens (the same code I have locally, code is very simple). I can't publish my app with this bug :/ pls help if you have any idea – Przemo May 09 '20 at 23:03
0

One way to detect when the view comes from the background is to set callbacks on the router outlet (in angular will be)

<page-router-outlet
        (loaded)="outletLoaded($event)"
        (unloaded)="outletUnLoaded($event)"></page-router-outlet>

Then you cn use outletLoaded(args: EventData) {} to initialise your code respectively outletUnLoaded to destroy your subscriptions.

This is helpful in cases where you have access to the router outlet (in App Component for instance)

In case when you are somewhere inside the navigation tree you can listen for suspend event

Application.on(Application.suspendEvent, (data: EventData) => {
      this.backFromBackground = true;
    });

Then when opening the app if the flag is true it will give you a hint that you are coming from the background rather than opening for the first time.

It works pretty well for me. Hope that help you as well.

Hypnotic
  • 21
  • 4