Giter Club home page Giter Club logo

dynamo's People

Contributors

doridori avatar mpp-doric avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

dynamo's Issues

Annotation driven to remove boilerplate Dynamo hookups

There is a small amount of boilerplate for observing changes inside view components. Would be interesting to see if this can be made easier using annotations.

Wary as don't want to turn it into a magic box which has a whole load of stuff going on thats not obvious.

Depending on the use-cases part of the Dyanmo / Dynamo holder hookup is related to hooking into the lifecycle (and part is UUID storing). This also could be annotation driven OR could have the non-ui fragment approach to hook into lifecycle ala Glide

F.A.Q

create F.A.Q with questions people are asking through issues and add to wiki.

Some thouhts/questions on Dynamo

First off, the wiki (first 5 pages) and blog posts were fantastic reads. Definitely the best I've read on this subject. I will definitely be returning to this material to think more about what you're proposed.

After reading, I had a few questions:

On page 5, you say that:

Its important that each state should be able to expose its own interface, for example ComputationFinishedState has a getResult() method. This is why we have used the Visitor pattern as it allows us to do this easily.

  1. I had a little trouble seeing how the Visitor pattern makes it easier for each State to expose its own interface. Why can't a simple callback do this as well as a visitor?
  2. I want to make sure I've understood how the objects in this architecture "hang together," so I want to try to apply your approach to a scenario that didn't seem to be explicitly covered in your wiki. Suppose you want to do something when an Activity's onStop() method is called. Suppose further that the code you want to execute has a lot of business logic in it. On your approach, that business logic wouldn't live in the Activity. Instead, you'd call a method on a dynamo which would then call a method on a State and the State would handle the business logic. Does that sound like the dynamo-way of handling that scenario?

How to handle callback from the DB layer

In my app I have 3 main layers:

  • Presentation layer - which includes: activities, fragments, dialogs etc..
  • Dynamo layer - which has all dynamos and the dynamo manager
  • DB layer - which communicates with different sources

I have a list with various items, and I want to delete one of them.
Currently, the flow is:

  • The fragment calls deleteItem() method of the dynamo
  • The dynamo calls deleteItem() of the current state "PendingState"
  • The state calls deleteItem() of the DB layer and waits for the success callback
  • When the success callback is called. I switch to another state ("DeleteItemState") which its only purpose is to make the fragment delete the item.
  • In the fragment, when the onState of the "DeleteItemState" called, I remove the item from the adapter and calls the dynamo to switch again to the PendingState

My solution works, but I guess you have better idea of how to handle callbacks from the DB layer without dedicated state.

Thanks a lot!

A Dynamo that extends Rx.Observable

Need to have a think about if this would add any benefit. Interested in hooking up with RxAndroid to remove the boilerplate of adding and removing observers based upon the views lifecycle. One for another day!

(suggestion) Activity.isFinishing() in the docs

Hi,

In Design scenarios, Part 3, 3. Screen by screen, I think using Activity.isFinishing() to check if an activity is actually finishing or if a configuration change is happening would be a better approach. Please tell me if I missed some usecase where it would not work.

Thanks for this project by the way, the documentation is really good, looks promising!

More Examples needed

  • Saving state on process death
  • Removing Dynamo on lifecycle based timer. Useful for memory footprint and or security requirements
  • WeakRef based DynamoHolder
  • Dynamos in listView
  • Multiple views on same Dynamo
  • Dynamo hooked up to View for Fixed, Variable and UUID (instance based) meta-key data
  • Example application & blog post

Should add some tests in for these also

Change focus of project

I am finding that Dynamo is nice to use as the state machine component of a wider arch like https://github.com/doridori/Pilot. This can be bundled inside a presenter and used to represent the state which can be easily queried by a lifecycle-affected view.

Should change the focus of project as something thats useful inside presenters (or state machine in general). Changes involve

  • Deprecating part of wiki. Lots of wiki is talking about dynamo as a complete arch in itself. Point to pilot where needed.
  • Creating an extendable LCE (loading/content/error) state machine as this is most common use case. (maybe simpler for a strict LCE not to have a state machine behind the scenes anyhow)
  • DynamoManager (really a Map) should prob be optional extra as only used when you have a presenter hosting child presenters for child viewa

wiki spelling/grammer

Not sure if this interests you, but here are some very minor misspellings I noticed in the wiki:

page 4. You extend=s= this for each
page 5. It is a singleton tha=n= handles
page 8. It='=s great and I recommend =it=
page 8. Activit=y=s, not sure if Activities is more appropriate here
page 4. "5. Design Scenarios (Recipes)" link is broken

onState called several times after switch state

I call newState() from one of my states, but the onState callback is called twice on my fragment.
Also, on screen rotation, the onState callback is called several time too.

Steps tp reproduce:

  • Current state = RegisterPendingState
  • Call register method from the Fragment with params which will fail the validations (empty email)
  • RegisterPendingState will create new state -> RegisterLoadingState
  • RegisterLoadingState will validate to data in enteringState and will create a new state (RegisterFailedState) in case of an error.
  • Finally, the onState(RegisterFailedState) will be called twice in the AuthFragment

My Dynamo:

package com.weshopit.android.dynamo;

import android.support.annotation.StringRes;

import com.weshopit.android.R;
import com.weshopit.android.data.DataSourceImpl;
import com.weshopit.android.data.callbacks.LoginListener;
import com.weshopit.android.data.callbacks.RegisterListener;

import couk.doridori.dynamo.Dynamo;
import couk.doridori.dynamo.StateMachine;

public class AuthDynamo extends Dynamo<AuthDynamo.AuthState> {


    // ======================================================================================
    // CONSTRUCTOR
    // ======================================================================================
    public AuthDynamo(){
        newState(new LoginPendingState());
    }

    // ======================================================================================
    // METHODS
    // ======================================================================================

    public void showLogin() {
        getStateMachine().getCurrentState().showLogin();
    }
    public void showRegistration() {
        getStateMachine().getCurrentState().showRegistration();
    }

    public void login(String email, String password) {
        getStateMachine().getCurrentState().login(email, password);
    }

    public void register(String email, String name, String password, String passwordConfirmation) {
        getStateMachine().getCurrentState().register(email, name, password, passwordConfirmation);
    }

    public void errorAcknowledged() {
        getStateMachine().getCurrentState().errorAcknowledged();
    }

    // ======================================================================================
    // STATES
    // ======================================================================================

    protected abstract class AuthState extends StateMachine.State{

        protected String className = getClass().getName();

        public void login(String email, String password){
            throw new IllegalStateException("State " + className + " cannot login");
        }

        public void register(String email, String name, String password, String passwordConfirmation){
            throw new IllegalStateException("State " + className + " cannot register");
        }

        public void errorAcknowledged(){
            throw new IllegalStateException("State " + className + " cannot acknowledge error");
        }

        public void showLogin() {
            newState(new LoginPendingState());
        }

        public void showRegistration() {
            newState(new RegisterPendingState());
        }

        /**
         * Visitor pattern
         */
        public abstract void accept(AuthVisitor visitor);
    }

    public class LoginPendingState extends AuthState {

        @Override
        public void login(String email, String password) {
            newState(new LoginLoadingState(email, password));
        }

        @Override
        public void accept(AuthVisitor visitor){
            visitor.onState(this);
        }
    }

    public class LoginLoadingState extends AuthState {

        private final String mEmail;
        private final String mPassword;

        public LoginLoadingState(String email, String password){
            mEmail = email;
            mPassword = password;
        }

        @Override
        public void enteringState() {

            int error = 0;

            if(mEmail.isEmpty()) {
                error = R.string.error_empty_email;
            }

            if(mPassword.isEmpty()) {
                error = R.string.error_empty_password;
            }

            if(error != 0){
                newState(new LoginFailedState(error));
                return;
            }

            DataSourceImpl.getInstance().login(mEmail, mPassword, new LoginListener() {
                @Override
                public void onLoginSucceeded(String access_token, String name, String email, int expires_in) {
                    newState(new LoginSuccessState());
                }

                @Override
                public void onLoginFailed(String error) {
                    newState(new LoginFailedState(error));
                }
            });
        }

        @Override
        public void accept(AuthVisitor visitor){
            visitor.onState(this);
        }
    }

    public class LoginSuccessState extends AuthState {

        @Override
        public void accept(AuthVisitor visitor){
            visitor.onState(this);
        }
    }

    public class LoginFailedState extends AuthState {

        String mErrorText;
        int mErrorRes;

        public LoginFailedState(String error){
            mErrorText = error;
        }

        public LoginFailedState(@StringRes int error){
            mErrorRes = error;
        }

        public String getErrorText(){
            return mErrorText;
        }

        public int getErrorRes(){
            return mErrorRes;
        }

        @Override
        public void errorAcknowledged() {
            newState(new LoginPendingState());
        }

        @Override
        public void accept(AuthVisitor visitor){
            visitor.onState(this);
        }
    }

    public class RegisterPendingState extends AuthState {

        @Override
        public void register(String email, String name, String password, String passwordConfirmation) {
            newState(new RegisterLoadingState(email, name, password, passwordConfirmation));
        }

        @Override
        public void accept(AuthVisitor visitor){
            visitor.onState(this);
        }
    }

    public class RegisterLoadingState extends AuthState {

        private final String mEmail;
        private final String mName;
        private final String mPassword;
        private final String mPasswordConfirmation;

        public RegisterLoadingState(String email, String name, String password, String passwordConfirmation){
            mEmail = email;
            mName = name;
            mPassword = password;
            mPasswordConfirmation = passwordConfirmation;
        }

        @Override
        public void enteringState() {

            int error = 0;

            if(mEmail.isEmpty()) {
                error = R.string.error_empty_email;
            }

            if(mPassword.isEmpty()) {
                error = R.string.error_empty_password;
            }

            if(mName.isEmpty()) {
                error = R.string.error_empty_name;
            }

            if(!mPassword.equals(mPasswordConfirmation)) {
                error = R.string.error_password_not_match;
            }

            if(error != 0){
                newState(new RegisterFailedState(error));
                return;
            }

            // Perform registration
            DataSourceImpl.getInstance().register(mEmail, mName, mPassword, new RegisterListener() {
                @Override
                public void onRegistrationsSucceeded(String email, String password) {
                    newState(new RegisterSuccessState());
                }

                @Override
                public void onRegistrationFailed(String error) {
                    newState(new RegisterFailedState(error));
                }
            });
        }

        @Override
        public void accept(AuthVisitor visitor){
            visitor.onState(this);
        }
    }

    public class RegisterSuccessState extends AuthState {

        @Override
        public void accept(AuthVisitor visitor){
            visitor.onState(this);
        }
    }

    public class RegisterFailedState extends AuthState {

        String mErrorText;
        int mErrorRes;

        public RegisterFailedState(String error){
            mErrorText = error;
        }

        public RegisterFailedState(@StringRes int error){
            mErrorRes = error;
        }

        public String getErrorText(){
            return mErrorText;
        }

        public int getErrorRes(){
            return mErrorRes;
        }

        @Override
        public void errorAcknowledged() {
            newState(new RegisterPendingState());
        }

        @Override
        public void accept(AuthVisitor visitor){
            visitor.onState(this);
        }
    }

    //======================================================================================
    // VISITOR
    //======================================================================================

    public interface AuthVisitor {
        void onState(LoginPendingState state);
        void onState(LoginLoadingState state);
        void onState(LoginSuccessState state);
        void onState(LoginFailedState state);
        void onState(RegisterPendingState state);
        void onState(RegisterLoadingState state);
        void onState(RegisterSuccessState state);
        void onState(RegisterFailedState state);
    }

    public void visitCurrentState(AuthVisitor visitor){
        getStateMachine().getCurrentState().accept(visitor);
    }
}

My Fragment:

package com.weshopit.android.presentation.fragments;

import android.app.Activity;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;

import com.weshopit.android.R;
import com.weshopit.android.dynamo.AuthDynamo;
import com.weshopit.android.dynamo.DynamoManager;
import com.weshopit.android.presentation.activities.AuthActivity;
import com.weshopit.android.presentation.core.BaseFragment;
import com.weshopit.android.presentation.customViews.EmailHistoryAutoComplete;
import com.weshopit.android.presentation.utils.Navigator;

import java.util.List;
import java.util.Observable;
import java.util.Observer;

import butterknife.ButterKnife;
import butterknife.InjectView;
import butterknife.InjectViews;
import butterknife.OnClick;

public class AuthFragment extends BaseFragment implements
        Observer, AuthDynamo.AuthVisitor {

    @InjectView(R.id.fragment_auth_et_email)
    EmailHistoryAutoComplete mEmailEt;

    @InjectView(R.id.fragment_auth_et_name)
    EditText mNameEt;

    @InjectView(R.id.fragment_auth_et_pass)
    EditText mPassEt;

    @InjectView(R.id.fragment_auth_pass_confirmation)
    EditText mPassConfirmationEt;

    @InjectViews({
            R.id.fragment_auth_et_name,
            R.id.fragment_auth_pass_confirmation,
            R.id.fragment_auth_btn_register,
            R.id.fragment_auth_btn_switch_to_login,
            R.id.fragment_auth_btn_skip})
    List<View> registrationViews;

    @InjectViews({
            R.id.fragment_auth_btn_login,
            R.id.fragment_auth_btn_switch_to_register,
            R.id.fragment_auth_btn_forget_pass })
    List<View> loginViews;

    private AuthActivity mActivity;
    private AuthDynamo mDynamo;

    private boolean isViewCreated = false;

    // ====================================================
    // Lifecycle
    // ====================================================
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_auth, container, false);

        ButterKnife.inject(this, rootView);

        isViewCreated = true;
        init();

        return rootView;
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        mActivity = (AuthActivity) getActivity();

        init();
    }


    @Override
    public void onDetach() {
        super.onDetach();
        mActivity = null;
    }

    /**
     * Initialize dynamo only after the fragemtn is attached and the view were created
     */
    private void init(){
        if(isViewCreated && mActivity != null){
            mDynamo = DynamoManager.getInstance().getAuthDynamo();
            mDynamo.addObserver(this);
            mDynamo.visitCurrentState(this);
        }
    }

    public void onDisplay(){
        mEmailEt.requestFocus();
    }

    // ====================================================
    // Public methods
    // ====================================================
    public void showLogin() {
        mDynamo.showLogin();
    }

    public void showRegistration() {
        mDynamo.showRegistration();
    }

    // ====================================================
    // UI events
    // ====================================================

    @OnClick(R.id.fragment_auth_btn_switch_to_register)
    public void switchToRegistration() {
        // Switch from login to register and vice versa
        mDynamo.showRegistration();
    }
    @OnClick(R.id.fragment_auth_btn_switch_to_login)
    public void switchToLogin() {
        // Switch from login to register and vice versa
        mDynamo.showLogin();
    }

    @OnClick(R.id.fragment_auth_btn_skip)
    public void skipLogin() {
        mActivity.showSkipAuthApproval();
    }

    @OnClick(R.id.fragment_auth_btn_forget_pass)
    public void forgetPassword() {
        mActivity.showForgetPassword(mEmailEt.getText().toString());
    }

    @OnClick(R.id.fragment_auth_btn_login)
    public void login() {
        mDynamo.login(mEmailEt.getText().toString(), mPassEt.getText().toString());
    }

    @OnClick(R.id.fragment_auth_btn_register)
    public void register() {
        mDynamo.register(
                mEmailEt.getText().toString(),
                mNameEt.getText().toString(),
                mPassEt.getText().toString(),
                mPassConfirmationEt.getText().toString()
        );
    }

    //======================================================================================
    // Dynamo States
    //======================================================================================
    // the below onState methods are where we should perform all UI transitions.

    @Override
    public void onState(AuthDynamo.LoginPendingState state) {
        ButterKnife.apply(registrationViews, HIDE);
        ButterKnife.apply(loginViews, SHOW);
    }

    @Override
    public void onState(AuthDynamo.LoginLoadingState state) {
        if(mActivity != null){
            mActivity.showProgress();
        }
    }

    @Override
    public void onState(AuthDynamo.LoginSuccessState state) {
        if(mActivity != null){
            Navigator.toHome(mActivity);
            DynamoManager.getInstance().mAuthDynamoHolder.clearAll();
        }
    }

    @Override
    public void onState(AuthDynamo.LoginFailedState state) {
        if(mActivity != null) {
            mActivity.hideProgress();

            String errorText = state.getErrorText();
            if(errorText == null){
                errorText = getString(state.getErrorRes());
            }

            mActivity.showError(errorText, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    mDynamo.errorAcknowledged();
                }
            });
        }
    }

    @Override
    public void onState(AuthDynamo.RegisterPendingState state) {
        ButterKnife.apply(registrationViews, SHOW);
        ButterKnife.apply(loginViews, HIDE);
    }

    @Override
    public void onState(AuthDynamo.RegisterLoadingState state) {
        if(mActivity != null){
            mActivity.showProgress();
        }
    }

    @Override
    public void onState(AuthDynamo.RegisterSuccessState state) {
        if(mActivity != null){
            Navigator.toHome(mActivity);
            DynamoManager.getInstance().mAuthDynamoHolder.clearAll();
        }
    }

    @Override
    public void onState(AuthDynamo.RegisterFailedState state) {
        if(mActivity != null) {
            mActivity.hideProgress();

            String error = state.getErrorText();
            if(error == null){
                error = getString(state.getErrorRes());
            }

            mActivity.showError(error, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    mDynamo.errorAcknowledged();
                }
            });
        }
    }

    //======================================================================================
    // Observe controller changes
    //======================================================================================

    @Override
    public void update(Observable observable, Object data){
        // visiting the current state will result in one of the onState() methods in this class to be called
        mDynamo.visitCurrentState(this);
    }

    // ====================================================
    // Internal methods
    // ====================================================
    static final ButterKnife.Action<View> SHOW = new ButterKnife.Action<View>() {
        @Override public void apply(View view, int index) {
            view.setVisibility(View.VISIBLE);
        }
    };
    static final ButterKnife.Action<View> HIDE = new ButterKnife.Action<View>() {
        @Override public void apply(View view, int index) {
            view.setVisibility(View.GONE);
        }
    };
}

Layout:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity"
    android:background="#ffffff">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <Button
            android:id="@+id/fragment_auth_btn_switch_to_login"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_alignParentEnd="true"
            android:text="@string/login"
            />

        <Button
            android:id="@+id/fragment_auth_btn_switch_to_register"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_alignParentEnd="true"
            android:text="@string/register"
            />

        <Button
            android:id="@+id/fragment_auth_btn_forget_pass"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:text="@string/forget_password"
            />

        <Button
            android:id="@+id/fragment_auth_btn_skip"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:text="@string/skip_auth" />

    </RelativeLayout>

    <com.weshopit.android.presentation.customViews.EmailHistoryAutoComplete
        android:id="@+id/fragment_auth_et_email"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="textEmailAddress"
        android:padding="10dp"
        android:hint="@string/email" />

    <EditText
        android:id="@+id/fragment_auth_et_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:hint="@string/name"
        />

    <EditText
        android:id="@+id/fragment_auth_et_pass"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="textPassword"
        android:layout_marginTop="-2dp"
        android:padding="10dp"
        android:hint="@string/password"
        />

    <EditText
        android:id="@+id/fragment_auth_pass_confirmation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="textPassword"
        android:layout_marginTop="-2dp"
        android:padding="10dp"
        android:hint="@string/confirmation"
        />

    <Button
        android:id="@+id/fragment_auth_btn_login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:layout_margin="4dp"
        android:text="@string/sign_in"
        />

    <Button
        android:id="@+id/fragment_auth_btn_register"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:layout_margin="4dp"
        android:text="@string/register"
        />

</LinearLayout>

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.