listContacts() {
49 | return unmodifiableList(new ArrayList<>(contacts.values()));
50 | }
51 |
52 | public Contact getContact(String id) {
53 | return contacts.get(id);
54 | }
55 |
56 | public void save(Contact contact) {
57 | contacts.put(contact.id, contact);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/flow-sample-tree/src/main/java/flow/sample/tree/ui/contacts/list/ListContactsView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Square Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package flow.sample.tree.ui.contacts.list;
18 |
19 | import android.content.Context;
20 | import android.util.AttributeSet;
21 | import android.view.View;
22 | import android.widget.AdapterView;
23 | import android.widget.ListView;
24 | import flow.Flow;
25 | import flow.sample.tree.FlowServices;
26 | import flow.sample.tree.model.Contact;
27 | import flow.sample.tree.model.ContactsStorage;
28 | import flow.sample.tree.ui.contacts.edit.EditNameScreen;
29 |
30 | public class ListContactsView extends ListView {
31 |
32 | private final ContactsAdapter adapter = new ContactsAdapter();
33 |
34 | private ContactsStorage storage;
35 |
36 | public ListContactsView(Context context, AttributeSet attrs) {
37 | super(context, attrs);
38 | storage = Flow.getService(FlowServices.CONTACTS_STORAGE, context);
39 | }
40 |
41 | @Override protected void onFinishInflate() {
42 | super.onFinishInflate();
43 | setAdapter(adapter);
44 | setOnItemClickListener(new AdapterView.OnItemClickListener() {
45 | @Override public void onItemClick(AdapterView> parent, View view, int position, long id) {
46 | Contact contact = adapter.getItem(position);
47 | Flow.get(ListContactsView.this).set(new EditNameScreen(contact.id));
48 | }
49 | });
50 | }
51 |
52 | @Override protected void onAttachedToWindow() {
53 | super.onAttachedToWindow();
54 | adapter.setContacts(storage.listContacts());
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/flow/src/main/java/flow/Dispatcher.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Square Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package flow;
18 |
19 | import android.support.annotation.NonNull;
20 |
21 | public interface Dispatcher {
22 | /**
23 | * Called when the history is about to change. Note that Flow does not consider the
24 | * Traversal to be finished, and will not actually update the history, until the callback is
25 | * triggered. Traversals cannot be canceled.
26 | *
27 | * Also called immediately after {@link Flow#setDispatcher}, to update the new dispatcher
28 | * to Flow's current state. Such bootstrap Traversals have a null {@link Traversal#origin},
29 | * and {@link Direction#REPLACE} as their direction. It should be noted that the dispatcher
30 | * is set and unset each time the app pauses and resumes, meaning the dispatcher will receive
31 | * a bootstrap call each time the app is activated.
32 | *
33 | * Dispatchers are required to be idempotent. They should check whether the app is already in
34 | * the correct state for the incoming key before performing any redundant work. (This probably
35 | * includes comparing the {@link Flow#getKey(android.view.View) key of the currently visible
36 | * view(s)} to that in {@link Traversal#destination} before doing any unnecessary inflation
37 | * and rendering). If no update is needed, short circuit
38 | * by firing the callback immediately and returning.
39 | *
40 | * @param callback Must be called to indicate completion of the traversal.
41 | */
42 | void dispatch(@NonNull Traversal traversal, @NonNull TraversalCallback callback);
43 | }
44 |
--------------------------------------------------------------------------------
/flow-sample-tree/src/main/java/flow/sample/tree/ui/contacts/list/ContactsAdapter.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Square Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package flow.sample.tree.ui.contacts.list;
18 |
19 | import android.view.LayoutInflater;
20 | import android.view.View;
21 | import android.view.ViewGroup;
22 | import android.widget.BaseAdapter;
23 | import android.widget.TextView;
24 | import flow.sample.tree.R;
25 | import flow.sample.tree.model.Contact;
26 | import java.util.ArrayList;
27 | import java.util.List;
28 |
29 | final class ContactsAdapter extends BaseAdapter {
30 | private final List contacts = new ArrayList<>();
31 |
32 | public void setContacts(List contacts) {
33 | this.contacts.clear();
34 | this.contacts.addAll(contacts);
35 | notifyDataSetChanged();
36 | }
37 |
38 | @Override public int getCount() {
39 | return contacts.size();
40 | }
41 |
42 | @Override public Contact getItem(int position) {
43 | return contacts.get(position);
44 | }
45 |
46 | @Override public long getItemId(int position) {
47 | return position;
48 | }
49 |
50 | @Override public View getView(int position, View convertView, ViewGroup parent) {
51 | Contact contact = getItem(position);
52 | View view = convertView;
53 | if (view == null) {
54 | final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
55 | view = inflater.inflate(R.layout.list_contacts_screen_row_view, parent, false);
56 | }
57 | ((TextView) view.findViewById(R.id.contact_name)).setText(contact.name);
58 | ((TextView) view.findViewById(R.id.contact_email)).setText(contact.email);
59 | return view;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/flow/src/main/java/flow/InternalContextWrapper.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Square Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package flow;
18 |
19 | import android.app.Activity;
20 | import android.content.Context;
21 | import android.content.ContextWrapper;
22 | import android.support.annotation.Nullable;
23 |
24 | final class InternalContextWrapper extends ContextWrapper {
25 | private static final String FLOW_SERVICE = "flow.InternalContextWrapper.FLOW_SERVICE";
26 | private static final String CONTEXT_MANAGER_SERVICE =
27 | "flow.InternalContextWrapper.CONTEXT_MANAGER_SERVICE";
28 |
29 | @Nullable static Flow getFlow(Context context) {
30 | @SuppressWarnings("WrongConstant")
31 | Flow systemService = (Flow) context.getSystemService(FLOW_SERVICE);
32 | return systemService;
33 | }
34 |
35 | static KeyManager getContextManager(Context context) {
36 | @SuppressWarnings("WrongConstant")
37 | final KeyManager service =
38 | (KeyManager) context.getSystemService(CONTEXT_MANAGER_SERVICE);
39 | return service;
40 | }
41 |
42 | private final Activity activity;
43 | private Flow flow;
44 | private KeyManager keyManager;
45 |
46 | InternalContextWrapper(Context baseContext, Activity activity) {
47 | super(baseContext);
48 | this.activity = activity;
49 | }
50 |
51 | @Override public Object getSystemService(String name) {
52 | if (FLOW_SERVICE.equals(name)) {
53 | if (flow == null) {
54 | flow = InternalLifecycleIntegration.require(activity).flow;
55 | }
56 | return flow;
57 | } else if (CONTEXT_MANAGER_SERVICE.equals(name)) {
58 | if (keyManager == null) {
59 | keyManager = InternalLifecycleIntegration.require(activity).keyManager;
60 | }
61 | return keyManager;
62 | }
63 | return super.getSystemService(name);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/flow-sample-tree/src/main/java/flow/sample/tree/FlowServices.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Square Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package flow.sample.tree;
18 |
19 | import android.support.annotation.NonNull;
20 | import flow.Services;
21 | import flow.ServicesFactory;
22 | import flow.sample.tree.model.Contact;
23 | import flow.sample.tree.model.ContactEditor;
24 | import flow.sample.tree.model.ContactsStorage;
25 | import flow.sample.tree.ui.contacts.ContactsUiKey;
26 | import flow.sample.tree.ui.contacts.edit.EditContactKey;
27 |
28 | public final class FlowServices extends ServicesFactory {
29 | public static final String CONTACTS_STORAGE = "CONTACTS_STORAGE";
30 | public static final String CONTACT_EDITOR = "CONTACT_EDITOR";
31 |
32 | // In a real app, the conditional class matching shown here doesn't scale very far. Decompose by
33 | // keys. Even better, keep your ServicesFactory lean and simple by using the key to build/lookup
34 | // a Dagger graph or Mortar scope!
35 |
36 | @Override public void bindServices(@NonNull Services.Binder services) {
37 | Object key = services.getKey();
38 | if (key.equals(new ContactsUiKey())) {
39 | // Setting up the ContactsUiKey means providing storage for Contacts.
40 | services.bind(CONTACTS_STORAGE, new ContactsStorage());
41 | } else if (key instanceof EditContactKey) {
42 | // Setting up the EditContactKey key means providing an editor for the contact.
43 | // This editor can be shared among any keys that have the EditContactKey as parent/ancestor!
44 | final String contactId = ((EditContactKey) key).contactId;
45 | ContactsStorage storage = services.getService(CONTACTS_STORAGE);
46 | Contact contact = storage.getContact(contactId);
47 | services.bind(CONTACT_EDITOR, new ContactEditor(contact));
48 | }
49 | }
50 |
51 | @Override public void tearDownServices(Services services) {
52 | // Nothing to do in this example, but if you need this hook to release resources, it's here!
53 | super.tearDownServices(services);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/flow-sample-intents/src/androidTest/java/flow/sample/intents/IntentsSampleTest.java:
--------------------------------------------------------------------------------
1 | package flow.sample.intents;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.support.test.rule.ActivityTestRule;
6 | import flow.Flow;
7 | import flow.History;
8 | import flow.sample.intents.IntentsSingleInstanceSampleActivity;
9 | import flow.sample.intents.IntentsStandardSampleActivity;
10 | import flow.sample.intents.StringParceler;
11 | import java.util.List;
12 | import org.junit.Rule;
13 | import org.junit.Test;
14 |
15 | import static android.support.test.InstrumentationRegistry.getInstrumentation;
16 | import static android.support.test.espresso.Espresso.onView;
17 | import static android.support.test.espresso.action.ViewActions.click;
18 | import static android.support.test.espresso.action.ViewActions.pressBack;
19 | import static android.support.test.espresso.matcher.ViewMatchers.withText;
20 | import static java.util.Arrays.asList;
21 |
22 | /**
23 | * Demonstrates the use of Intents to drive espresso tests to specific screens.
24 | */
25 | public class IntentsSampleTest {
26 |
27 | @Rule public ActivityTestRule rule = new ActivityTestRule<>(IntentsSingleInstanceSampleActivity.class);
28 |
29 | @Test public void singleInstanceActivity() {
30 | List expected = asList("Able", "Baker", "Charlie");
31 | History h = History.emptyBuilder().pushAll(expected).build();
32 |
33 | Context context = getInstrumentation().getTargetContext().getApplicationContext();
34 | Intent intent = new Intent(context, IntentsSingleInstanceSampleActivity.class);
35 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
36 | Flow.addHistory(intent, h, new StringParceler());
37 | context.startActivity(intent);
38 |
39 | onView(withText("Charlie")).perform(pressBack());
40 | onView(withText("Baker")).perform(pressBack());
41 | onView(withText("Able")).perform(click());
42 | }
43 |
44 | @Test public void standardActivity() {
45 | List expected = asList("Higgledy", "Piggledy", "Pop");
46 | History h = History.emptyBuilder().pushAll(expected).build();
47 |
48 | Context context = getInstrumentation().getTargetContext().getApplicationContext();
49 | Intent intent = new Intent(context, IntentsStandardSampleActivity.class);
50 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
51 | Flow.addHistory(intent, h, new StringParceler());
52 | context.startActivity(intent);
53 |
54 | onView(withText("Pop")).perform(pressBack());
55 | onView(withText("Piggledy")).perform(pressBack());
56 | onView(withText("Higgledy")).perform(click());
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/flow/src/main/java/flow/Services.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Square Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package flow;
18 |
19 | import android.support.annotation.NonNull;
20 | import android.support.annotation.Nullable;
21 | import java.util.Collections;
22 | import java.util.LinkedHashMap;
23 | import java.util.Map;
24 |
25 | import static flow.Preconditions.checkNotNull;
26 |
27 | public class Services {
28 | static final Services ROOT_SERVICES =
29 | new Services(Flow.ROOT_KEY, null, Collections.emptyMap());
30 |
31 | public static final class Binder extends Services {
32 | private final Map services = new LinkedHashMap<>();
33 | private final Services base;
34 |
35 | private Binder(Services base, Object key) {
36 | super(key, base, Collections.emptyMap());
37 | checkNotNull(base, "only root Services should have a null base");
38 | this.base = base;
39 | }
40 |
41 | @NonNull public Binder bind(@NonNull String serviceName, @NonNull Object service) {
42 | services.put(serviceName, service);
43 | return this;
44 | }
45 |
46 | @NonNull Services build() {
47 | return new Services(getKey(), base, services);
48 | }
49 | }
50 |
51 | private final Object key;
52 | @Nullable private final Services delegate;
53 | private final Map localServices = new LinkedHashMap<>();
54 |
55 | private Services(Object key, @Nullable Services delegate, Map localServices) {
56 | this.delegate = delegate;
57 | this.key = key;
58 | this.localServices.putAll(localServices);
59 | }
60 |
61 | @Nullable public T getService(@NonNull String name) {
62 | if (localServices.containsKey(name)) {
63 | @SuppressWarnings("unchecked") //
64 | final T service = (T) localServices.get(name);
65 | return service;
66 | }
67 | if (delegate != null) return delegate.getService(name);
68 | return null;
69 | }
70 |
71 | @NonNull public T getKey() {
72 | //noinspection unchecked
73 | return (T) this.key;
74 | }
75 |
76 | @NonNull Binder extend(@NonNull Object key) {
77 | return new Binder(this, key);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/flow-sample-basic/src/main/java/flow/sample/basic/BasicDispatcher.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Square Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package flow.sample.basic;
18 |
19 | import android.app.Activity;
20 | import android.support.annotation.LayoutRes;
21 | import android.support.annotation.NonNull;
22 | import android.util.Log;
23 | import android.view.LayoutInflater;
24 | import android.view.View;
25 | import android.view.ViewGroup;
26 | import flow.Dispatcher;
27 | import flow.Flow;
28 | import flow.Traversal;
29 | import flow.TraversalCallback;
30 |
31 | final class BasicDispatcher implements Dispatcher {
32 |
33 | private final Activity activity;
34 |
35 | BasicDispatcher(Activity activity) {
36 | this.activity = activity;
37 | }
38 |
39 | @Override
40 | public void dispatch(@NonNull Traversal traversal, @NonNull TraversalCallback callback) {
41 | Log.d("BasicDispatcher", "dispatching " + traversal);
42 | Object destKey = traversal.destination.top();
43 |
44 | ViewGroup frame = (ViewGroup) activity.findViewById(R.id.basic_activity_frame);
45 |
46 | // We're already showing something, clean it up.
47 | if (frame.getChildCount() > 0) {
48 | final View currentView = frame.getChildAt(0);
49 |
50 | // Save the outgoing view state.
51 | if (traversal.origin != null) {
52 | traversal.getState(traversal.origin.top()).save(currentView);
53 | }
54 |
55 | // Short circuit if we would just be showing the same view again.
56 | final Object currentKey = Flow.getKey(currentView);
57 | if (destKey.equals(currentKey)) {
58 | callback.onTraversalCompleted();
59 | return;
60 | }
61 |
62 | frame.removeAllViews();
63 | }
64 |
65 | @LayoutRes final int layout;
66 | if (destKey instanceof HelloScreen) {
67 | layout = R.layout.hello_screen;
68 | } else if (destKey instanceof WelcomeScreen) {
69 | layout = R.layout.welcome_screen;
70 | } else {
71 | throw new AssertionError("Unrecognized screen " + destKey);
72 | }
73 |
74 | View incomingView = LayoutInflater.from(traversal.createContext(destKey, activity)) //
75 | .inflate(layout, frame, false);
76 |
77 | frame.addView(incomingView);
78 | traversal.getState(traversal.destination.top()).restore(incomingView);
79 |
80 | callback.onTraversalCompleted();
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/flow-sample-tree/src/main/java/flow/sample/tree/ui/contacts/edit/EditNameView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Square Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package flow.sample.tree.ui.contacts.edit;
18 |
19 | import android.content.Context;
20 | import android.text.Editable;
21 | import android.text.TextWatcher;
22 | import android.util.AttributeSet;
23 | import android.view.KeyEvent;
24 | import android.widget.EditText;
25 | import android.widget.LinearLayout;
26 | import android.widget.TextView;
27 | import flow.Flow;
28 | import flow.sample.tree.FlowServices;
29 | import flow.sample.tree.R;
30 | import flow.sample.tree.model.ContactEditor;
31 |
32 | public class EditNameView extends LinearLayout {
33 |
34 | private ContactEditor editor;
35 | private TextView emailView;
36 | private EditText nameView;
37 | private TextWatcher nameWatcher;
38 |
39 | public EditNameView(Context context, AttributeSet attrs) {
40 | super(context, attrs);
41 | setOrientation(VERTICAL);
42 | editor = Flow.getService(FlowServices.CONTACT_EDITOR, getContext());
43 | }
44 |
45 | @Override protected void onFinishInflate() {
46 | super.onFinishInflate();
47 | emailView = (TextView) findViewById(R.id.email);
48 | nameView = (EditText) findViewById(R.id.edit_name);
49 | }
50 |
51 | @Override protected void onAttachedToWindow() {
52 | super.onAttachedToWindow();
53 | emailView.setText(editor.email);
54 | nameView.setText(editor.name);
55 | nameWatcher = new TextWatcher() {
56 | @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {
57 | }
58 |
59 | @Override public void onTextChanged(CharSequence s, int start, int before, int count) {
60 | editor.name = s.toString();
61 | }
62 |
63 | @Override public void afterTextChanged(Editable s) {
64 | }
65 | };
66 | nameView.addTextChangedListener(nameWatcher);
67 |
68 | nameView.setOnEditorActionListener(new TextView.OnEditorActionListener() {
69 | @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
70 | Flow.get(v).set(new EditEmailScreen(editor.id));
71 | return true;
72 | }
73 | });
74 | }
75 |
76 | @Override protected void onDetachedFromWindow() {
77 | nameView.setOnEditorActionListener(null);
78 | nameView.removeTextChangedListener(nameWatcher);
79 | super.onDetachedFromWindow();
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/flow/src/main/java/flow/Installer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Square Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package flow;
18 |
19 | import android.app.Activity;
20 | import android.app.Application;
21 | import android.content.Context;
22 | import android.support.annotation.NonNull;
23 | import android.support.annotation.Nullable;
24 | import java.util.ArrayList;
25 | import java.util.List;
26 |
27 | public final class Installer {
28 |
29 | private final Context baseContext;
30 | private final Activity activity;
31 | private final List contextFactories = new ArrayList<>();
32 | private KeyParceler parceler;
33 | private Object defaultKey;
34 | private Dispatcher dispatcher;
35 |
36 | Installer(Context baseContext, Activity activity) {
37 | this.baseContext = baseContext;
38 | this.activity = activity;
39 | }
40 |
41 | @NonNull public Installer keyParceler(@Nullable KeyParceler parceler) {
42 | this.parceler = parceler;
43 | return this;
44 | }
45 |
46 | @NonNull public Installer dispatcher(@Nullable Dispatcher dispatcher) {
47 | this.dispatcher = dispatcher;
48 | return this;
49 | }
50 |
51 | @NonNull public Installer defaultKey(@Nullable Object defaultKey) {
52 | this.defaultKey = defaultKey;
53 | return this;
54 | }
55 |
56 | /**
57 | * Applies a factory when creating a Context associated with a given key.
58 | *
59 | * May be called multiple times. Factories are called in the order given during setup, and
60 | * in reverse order during teardown.
61 | */
62 | @NonNull public Installer addServicesFactory(@NonNull ServicesFactory factory) {
63 | contextFactories.add(factory);
64 | return this;
65 | }
66 |
67 | @NonNull public Context install() {
68 | if (InternalLifecycleIntegration.find(activity) != null) {
69 | throw new IllegalStateException("Flow is already installed in this Activity.");
70 | }
71 | Dispatcher dispatcher = this.dispatcher;
72 | if (dispatcher == null) {
73 | dispatcher = KeyDispatcher.configure(activity, new DefaultKeyChanger(activity)) //
74 | .build();
75 | }
76 | final Object defState = defaultKey == null ? "Hello, World!" : defaultKey;
77 |
78 | final History defaultHistory = History.single(defState);
79 | final Application app = (Application) baseContext.getApplicationContext();
80 | final KeyManager keyManager = new KeyManager(contextFactories);
81 | InternalLifecycleIntegration.install(app, activity, parceler, defaultHistory, dispatcher,
82 | keyManager);
83 | return new InternalContextWrapper(baseContext, activity);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/flow-sample-tree/src/main/java/flow/sample/tree/ui/contacts/edit/EditEmailView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Square Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package flow.sample.tree.ui.contacts.edit;
18 |
19 | import android.content.Context;
20 | import android.text.Editable;
21 | import android.text.TextWatcher;
22 | import android.util.AttributeSet;
23 | import android.view.View;
24 | import android.widget.EditText;
25 | import android.widget.LinearLayout;
26 | import android.widget.TextView;
27 | import flow.Flow;
28 | import flow.sample.tree.FlowServices;
29 | import flow.sample.tree.R;
30 | import flow.sample.tree.model.ContactEditor;
31 | import flow.sample.tree.model.ContactsStorage;
32 | import flow.sample.tree.ui.contacts.list.ListContactsScreen;
33 |
34 | public class EditEmailView extends LinearLayout {
35 |
36 | ContactEditor editor;
37 | private TextView nameView;
38 | private EditText emailView;
39 | private TextWatcher emailWatcher;
40 | private View saveButton;
41 |
42 | public EditEmailView(Context context, AttributeSet attrs) {
43 | super(context, attrs);
44 | setOrientation(VERTICAL);
45 | editor = Flow.getService(FlowServices.CONTACT_EDITOR, context);
46 | }
47 |
48 | @Override protected void onFinishInflate() {
49 | super.onFinishInflate();
50 | nameView = (TextView) findViewById(R.id.name);
51 | emailView = (EditText) findViewById(R.id.edit_email);
52 | saveButton = findViewById(R.id.save);
53 | }
54 |
55 | @Override protected void onAttachedToWindow() {
56 | super.onAttachedToWindow();
57 | nameView.setText(editor.name);
58 | emailView.setText(editor.email);
59 | emailWatcher = new TextWatcher() {
60 | @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {
61 | }
62 |
63 | @Override public void onTextChanged(CharSequence s, int start, int before, int count) {
64 | editor.email = s.toString();
65 | }
66 |
67 | @Override public void afterTextChanged(Editable s) {
68 | }
69 | };
70 | emailView.addTextChangedListener(emailWatcher);
71 |
72 | saveButton.setOnClickListener(new OnClickListener() {
73 | @Override public void onClick(View v) {
74 | editor.email = emailView.getText().toString();
75 | ContactsStorage storage = Flow.getService(FlowServices.CONTACTS_STORAGE, v.getContext());
76 | //noinspection ConstantConditions
77 | storage.save(editor.toContact());
78 | Flow.get(v).set(new ListContactsScreen());
79 | }
80 | });
81 | }
82 |
83 | @Override protected void onDetachedFromWindow() {
84 | saveButton.setOnClickListener(null);
85 | emailView.removeTextChangedListener(emailWatcher);
86 | super.onDetachedFromWindow();
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/flow/src/main/java/flow/KeyDispatcher.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Square Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package flow;
18 |
19 | import android.app.Activity;
20 | import android.content.Context;
21 | import android.support.annotation.NonNull;
22 | import android.support.annotation.Nullable;
23 | import java.util.Collections;
24 | import java.util.LinkedHashMap;
25 | import java.util.List;
26 | import java.util.Map;
27 |
28 | import static flow.Preconditions.checkNotNull;
29 |
30 | /**
31 | * A simple Dispatcher that only pays attention to the top keys on the incoming and outgoing
32 | * histories, and only executes a change if those top keys are not equal.
33 | */
34 | public final class KeyDispatcher implements Dispatcher {
35 |
36 | public static final class Builder {
37 | private final Activity activity;
38 | private final KeyChanger keyChanger;
39 |
40 | private Builder(Activity activity, KeyChanger keyChanger) {
41 | this.activity = activity;
42 | this.keyChanger = checkNotNull(keyChanger, "KeyChanger may not be null");
43 | }
44 |
45 | public Dispatcher build() {
46 | final KeyChanger keyChanger =
47 | this.keyChanger == null ? new DefaultKeyChanger(activity) : this.keyChanger;
48 | return new KeyDispatcher(activity, keyChanger);
49 | }
50 | }
51 |
52 | public static Builder configure(Activity activity, KeyChanger changer) {
53 | return new Builder(activity, changer);
54 | }
55 |
56 | private final Activity activity;
57 | private final KeyChanger keyChanger;
58 |
59 | private KeyDispatcher(Activity activity, KeyChanger keyChanger) {
60 | this.activity = activity;
61 | this.keyChanger = keyChanger;
62 | }
63 |
64 | @Override public void dispatch(@NonNull Traversal traversal, @NonNull TraversalCallback callback) {
65 | State inState = traversal.getState(traversal.destination.top());
66 | Object inKey = inState.getKey();
67 | State outState = traversal.origin == null ? null : traversal.getState(traversal.origin.top());
68 | Object outKey = outState == null ? null : outState.getKey();
69 |
70 | // TODO(#126): short-circuit may belong in Flow, since every Dispatcher we have implements it.
71 | if (inKey.equals(outKey)) {
72 | callback.onTraversalCompleted();
73 | return;
74 | }
75 |
76 | Map