Created
August 31, 2016 05:07
-
-
Save monzee/98bfb660bf6018b6e3f601fbe4b96e96 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface Contact { | |
class Person { | |
public final String name; | |
public final int phone; | |
public Person(String name, int phone) { | |
this.name = name; | |
this.phone = phone; | |
} | |
} | |
interface PersonRepository { | |
Person anyone(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface Dial { | |
interface View { | |
void toggleDialButton(boolean enabled); | |
void showName(String name); | |
void launchDialler(int number); | |
void tell(String message); | |
} | |
interface Presenter { | |
void bind(View view); | |
void unbind(); | |
void didPressDial(); | |
void fetchPerson(); | |
void personFetched(Contact.Person p); | |
} | |
class State { | |
// exposed here for simplicity. should use dagger and make this | |
// singleton-scoped instead. | |
public static State INSTANCE = new State(); | |
State() {} | |
String name; | |
int phone; | |
volatile boolean isFetching; | |
volatile FutureTask<?> task; | |
void done() { | |
task = null; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class DialPresenter implements Dial.Presenter { | |
@SuppressWarnings("ThrowableInstanceNeverThrown") | |
private static final Throwable NO_ERROR = new Throwable(); | |
private final Contact.PersonRepository persons; | |
private final Dial.State state; | |
private Dial.View view; | |
public DialPresenter(Dial.State s, Contact.PersonRepository repo) { | |
state = s; | |
persons = repo; | |
} | |
@Override | |
public void bind(Dial.View view) { | |
this.view = view; | |
update(); | |
FutureTask<?> future = state.task; | |
if (future != null) { | |
view.tell("activity was restarted while fetching"); | |
view.toggleDialButton(false); | |
schedule(future::get, (ok, err) -> { | |
// ok value is not useful here, check the error instead | |
if (err == NO_ERROR) { | |
update(); | |
} else if (!future.isCancelled()) { // interrupted | |
throw new RuntimeException(err); | |
} // do nothing if cancelled | |
}); | |
} else if (state.name == null) { | |
// first run/new process | |
view.tell("fetching"); | |
fetchPerson(); | |
} | |
} | |
@Override | |
public void unbind() { | |
view = null; | |
if (state.isFetching) { | |
FutureTask<?> future = state.task; | |
if (future != null) { | |
// awake the waiting get() because it's about to be replaced. | |
// all AsyncTasks are queued in the same thread (if you don't | |
// pass an Executor) so this is very important, otherwise the | |
// thread will be blocked forever. | |
future.cancel(false); | |
} | |
state.task = new FutureTask<>(state::done, null); | |
} | |
} | |
@Override | |
public void didPressDial() { | |
if (view != null) { | |
view.launchDialler(state.phone); | |
} | |
} | |
@SuppressWarnings("StatementWithEmptyBody") | |
@Override | |
public void fetchPerson() { | |
if (state.isFetching) { | |
return; | |
} | |
state.isFetching = true; | |
schedule(persons::anyone, (ok, err) -> { | |
if (ok != null) { | |
personFetched(ok); | |
} else if (err == NO_ERROR) { | |
// the service returned a null but no error for some reason | |
// something like this may happen with retrofit2 if the server | |
// is reachable but returned a 4xx response. | |
} else { | |
throw new RuntimeException(err); | |
} | |
}); | |
} | |
@Override | |
public void personFetched(Contact.Person p) { | |
state.name = p.name; | |
state.phone = p.phone; | |
state.isFetching = false; | |
FutureTask<?> future = state.task; | |
if (future != null) { | |
future.run(); | |
} else if (view != null) { | |
update(); | |
} | |
} | |
private void update() { | |
if (state.name != null) { | |
view.showName(state.name); | |
view.toggleDialButton(true); | |
} else { | |
view.showName("?"); | |
view.toggleDialButton(false); | |
} | |
} | |
private interface Producer<T> { | |
T apply() throws Throwable; | |
} | |
private interface BiConsumer<T, U> { | |
void apply(T t, U u); | |
} | |
// implemented here for simplicity. | |
// the presenter shouldn't have platform dependencies | |
// a scheduler interface should be injected instead | |
private static <T> void schedule( | |
Producer<T> job, | |
BiConsumer<T, Throwable> continuation | |
) { | |
new AsyncTask<Void, Void, T>() { | |
private Throwable error = NO_ERROR; | |
@Override | |
protected T doInBackground(Void... voids) { | |
try { | |
return job.apply(); | |
} catch (Throwable e) { | |
error = e; | |
return null; | |
} | |
} | |
@Override | |
protected void onPostExecute(T t) { | |
continuation.apply(t, error); | |
} | |
}.execute(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class DialView implements Dial.View { | |
private final TextView name; | |
private final Button dial; | |
private final Activity activity; | |
public DialView(Dial.Presenter p, Activity a) { | |
activity = a; | |
name = (TextView) a.findViewById(R.id.the_name); | |
dial = (Button) a.findViewById(R.id.do_dial); | |
dial.setOnClickListener(view -> p.didPressDial()); | |
} | |
@Override | |
public void toggleDialButton(boolean enabled) { | |
dial.setEnabled(enabled); | |
} | |
@Override | |
public void showName(String name) { | |
this.name.setText(name); | |
} | |
@Override | |
public void launchDialler(int number) { | |
Intent i = new Intent(activity, DiallerActivity.class); | |
i.putExtra("phoneNumber", number); | |
activity.startActivity(i); | |
} | |
@Override | |
public void tell(String message) { | |
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class MainActivity extends AppCompatActivity { | |
private static class SlowFake implements Contact.PersonRepository { | |
@Override | |
public Contact.Person anyone() { | |
try { | |
Thread.sleep(10000); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
return new Contact.Person("foobar", 1234567890); | |
} | |
} | |
private Dial.Presenter presenter; | |
private Dial.View view; | |
@Override | |
protected void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
setContentView(R.layout.activity_main); | |
presenter = new DialPresenter(Dial.State.INSTANCE, new SlowFake()); | |
view = new DialView(presenter, this); | |
} | |
@Override | |
protected void onResume() { | |
super.onResume(); | |
presenter.bind(view); | |
} | |
@Override | |
protected void onStop() { | |
super.onStop(); | |
presenter.unbind(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment