Skip to content

Instantly share code, notes, and snippets.

@monzee
Created August 31, 2016 05:07
Show Gist options
  • Save monzee/98bfb660bf6018b6e3f601fbe4b96e96 to your computer and use it in GitHub Desktop.
Save monzee/98bfb660bf6018b6e3f601fbe4b96e96 to your computer and use it in GitHub Desktop.
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();
}
}
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;
}
}
}
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();
}
}
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();
}
}
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