lyft / scoop

:icecream: micro framework for building view based modular Android applications.
Apache License 2.0
1.03k stars 79 forks source link

Proper way to expose a service to any view in a Scoop app #13

Closed elijahz closed 8 years ago

elijahz commented 8 years ago

Hello,

I'm really liking your framework here.

I've successfully implemented a service at "root" scope level, and I'd like your comments or "best practice" advice for implementing such such a service.

Below are my notes on implementing a service accessible from any ViewController via the root scoop. I have saved my questions for the end.

Following these notes I was a able to start a single instance of Duktape at application load, and provide it as a service to all of my views, I'm just not confident that I did it properly.

Create the Service

/scoop/MyService.java

package com.xxx.scoop;

import android.view.View;
import com.lyft.scoop.Scoop;

public class MyService {

    public static final String SERVICE_NAME = "isthisevenusedbytheprogrammer";

    private String myString;

    public MyService() {
        myString = "This is the default text.";
    }

    public void setMyString(String text) {
        myString = text;
    }

    public String getMyString() {
        return myString;
    }

    public static MyService fromScoop(Scoop scoop) {
        return scoop.findService(SERVICE_NAME);
    }

    public static MyService fromView(View view) {
        return Scoop.fromView(view).findService(SERVICE_NAME);
    }
}

Add the service to root scoop:

/MainActivity.java

package com.xxx;
//...
import com.xxx.scoop.MyService;

//...

            private Scoop getRootScoop() {
//...

            DaggerInjector activityInjector = new DaggerInjector(activityGraph);

            MyService mysrv = new MyService();

            rootScoop = new Scoop.Builder("root")
            .service(DaggerInjector.SERVICE_NAME, activityInjector)
            .service(MyService.SERVICE_NAME, mysrv)
            .build();

        Timber.d("rootscoop built. name: " + rootScoop.getName());

        MyService testMyService = rootScoop.findService(MyService.SERVICE_NAME);
        Timber.d("testMyService.getMyString():" + testMyService.getMyString());
        Timber.d("testMyService.setMyString('test'):" + testMyService.setMyString("test"));
        Timber.d("testMyService.getMyString():" + testMyService.getMyString());
            }
        return rootScoop;
    }

Add the service to a ViewController and use it:

/ui/MyUi/MyTestController.java

package com.xxx.ui.myui;
//...
import com.xxx.scoop.MyService;

//...

@ControllerModule(MyTestController.Module.class)
@EnterTransition(DownwardSlideTransition.class)
@ExitTransition(UpwardSlideTransition.class)
public class MyTestController extends ViewController {
    private MyService myService;

//...

    @Override
    public void attach(View view) {
        super.attach(view);
        this.view = view;
        String args = Screen.fromController(this).args();

        //this doesn't feel right... but it works.
        Timber.d("getScoop name: " + this.getScoop().getName()); //MyTestController
        Scoop parentScoop = this.getScoop().getParent();
        Timber.d("parentScoop: " + parentScoop.getName()); //DemosController
        this.myService = parentScoop.findService(MyService.SERVICE_NAME);

//...
@OnClick(R.id.whateverButton)
    public void onClickWhateverButton() {
        whateverTextView.setText(myService.getMyString());
}

My questions are:

  1. Is this the proper workflow to achieve this goal?
  2. I understand bundled persistence isn't available yet so if android kills my app in background, next time I open the app the services will be re-initialized. Is there a way to prevent this for root scoped services? Are there any other cases where the root scoop might forget my service?
  3. In the README.md you state:

Instead of adding individual services to your scoops, we recommend implementing dagger integration. In this case the only added service will be the dagger injector.

Is that what I've achieved here, or is there another way to go about it?

Thank you for all your work on this. Scoop is really excellent as I start to understand it better.

elijahz commented 8 years ago

I've done some research into Dagger, and I now understand why you suggest using dagger rather than the root scoop for app-wide services. My previous comment's notes are probably not the best way to provide a service to all views in Scoop.

Below I've recreated my notes using Dagger instead of modifying the root scoop.

Create the service

/scoop/MyService.java

package com.xxx.scoop;

import android.view.View;
import com.lyft.scoop.Scoop;

public class MyService {

    //I don't think this is required now, since we're using dagger instead
    public static final String SERVICE_NAME = "isthisevenusedbytheprogrammer";

    private String myString;
    private int myInt;

    public MyService() {
        myString = "This is the default text.";
        myInt = 0;
    }

    public void setMyString(String text) {
        myString = text;
    }

    public String getMyString() {
        return myString;
    }

    public void setMyInt (int number) {
        myInt = number;
    }

    public int getMyInt() {
        return myInt;
    }

    public static MyService fromScoop(Scoop scoop) {
        return scoop.findService(SERVICE_NAME);
    }

    public static MyService fromView(View view) {
        return Scoop.fromView(view).findService(SERVICE_NAME);
    }
}

Add the service to AppModule

/AppModule.java

package com.xxx;

//...

import com.xxx.scoop.MyService;

//...

    @Singleton
    @Provides
    Application provideApplication() {
        return app;
    }

    @Singleton
    @Provides
    MyService provideMyService() {
        MyService msv = new MyService();
        return msv;
    }
}

Inject and Use

/ui/MyUi/MyTestController.java


package com.xxx.ui.myui;

//...

import com.xxx.scoop.MyService;

//...

@ControllerModule(MyTestController.Module.class)
@EnterTransition(DownwardSlideTransition.class)
@ExitTransition(UpwardSlideTransition.class)
public class MyTestController extends ViewController {
    @Inject MyService myService;

//...

@OnClick(R.id.whateverButton)
    public void onClickWhateverButton() {

        int x = myService.getMyInt();
        myService.setMyInt(x + 1);
        whateverTextView.setText("previous myService.getMyInt(): " + x + ". added 1. new myService.getMyInt():" + myService.getMyInt());
}

Please confirm that this is the best practice, or advise what should be changed.

Thank you!

lexer commented 8 years ago

@elijahz Should be correct. Consider using constructor injection over property for ViewControllers.

elijahz commented 8 years ago

@lexer I understand, thank you.