doridori / dynamo Goto Github PK
View Code? Open in Web Editor NEWA lightweight state-based controller for Android
A lightweight state-based controller for Android
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
create F.A.Q with questions people are asking through issues and add to wiki.
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.
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?In my app I have 3 main layers:
I have a list with various items, and I want to delete one of them.
Currently, the flow is:
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!
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!
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!
View
for Fixed, Variable and UUID (instance based) meta-key dataShould add some tests in for these also
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
Need to refactor to make MAIN ToC more descriptive. Some important points which are often asked about have non-obvious locations in the Wiki. Add a non-alpha sorted ToC https://github.com/jonschlinkert/markdown-toc
Should show UI layer / Dynamo / states / framework interactions and comms between them
Should add to mvn central / bintray
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
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:
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>
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.