commit
commit
Don’t Make Android Bad… Again
Pedro Vicente
Mobile Software Craftsman
Apps since ~2009
Self-organising company
…
Most beautiful person…ever
HIRING
2008
2008
Android
Android is
launched
September 2008!
Android Market
Goes live October
2008
AsyncTask ruled the (app) world!
• Callback hell
• Memory leaks
• Spaghetti code
https://thinkmobiles.com/blog/mvp-vs-mvvm-android-patterns/
Every app from 2008 to…
From: https://labs.ribot.co.uk/android-application-architecture-8b6e34acda65
Way to much
responsibilities:
1.Call the API and / or
cache
2. Handle success or
error (of both)
3.Transform the data
returned from the API
4.What happens if the
device rotates?
5. How to test all this?
6. And another N
number of issues..
From: https://labs.ribot.co.uk/android-application-architecture-8b6e34acda65
Way to much
responsibilities:
1.Call the API and / or
cache
2. Handle success or
error (of both)
3.Transform the data
returned from the API
4.What happens if the
device rotates?
5. How to test all this?
6. And another N
number of issues..
still
From: https://labs.ribot.co.uk/android-application-architecture-8b6e34acda65
Way to much
responsibilities:
1.Call the API and / or
cache
2. Handle success or
error (of both)
3.Transform the data
returned from the API
4. How to test all this?
From: https://labs.ribot.co.uk/android-application-architecture-8b6e34acda65
Way to much
responsibilities:
1.Call the API and / or
cache
2. Handle success or
error (of both)
3.Transform the data
returned from the API
4. How to test all this?
How to get bald 101
Actually…Actually…
From: https://labs.ribot.co.uk/android-application-architecture-8b6e34acda65
Way to much
responsibilities:
1.Call the API and / or
cache
2. Handle success or
error (of both)
3.Transform the data
returned from the API
4. How to test all this?
How to get bald 101
public	class	MyTodoActivity	extends	Activity	{	
				...	
				public	void	onClickItem(String	itemId){	
								new	FetchTaskList().execute(itemId);	
				}	
				private	class	FetchItemDetail	extends	AsyncTask<String,	Void,	String>	{	
								@Override	
								protected	void	onPreExecute()	{	
											showLoading();	
								}		
								@Override	
								protected	String	doInBackground(String...	params)	{	
												if(cacheProvider.contains(params[0]){	
															return	cacheProvider.get(params[0]);	
												}	
												Result	result	=	apiProvider.getItemDetail(params[0])	
												if(result	!=	null	...	&&	result.success){	
															cacheProvider.set(params[0],	result);	
												}else{	
															Log.e(LOG_TAG,	"Houston	we	have	a	problem..");	
												}	
												return	result;	
								}	
								@Override	
								protected	void	onPostExecute(String	result)	{	
												if(results	!=	null){	
															showResults(transformResults(result));	
												}else{	
															showResultsError();	
												}	
								}	
				}	
}
public	class	MyTodoActivity	extends	Activity	{	
				...	
				public	void	onClickItem(String	itemId){	
								new	FetchTaskList().execute(itemId);	
				}	
				private	class	FetchItemDetail	extends	AsyncTask<String,	Void,	String>	{	
								@Override	
								protected	void	onPreExecute()	{	
											showLoading();	
								}		
								@Override	
								protected	String	doInBackground(String...	params)	{	
												if(cacheProvider.contains(params[0]){	
															return	cacheProvider.get(params[0]);	
												}	
												Result	result	=	apiProvider.getItemDetail(params[0])	
												if(result	!=	null	...	&&	result.success){	
															cacheProvider.set(params[0],	result);	
												}else{	
															Log.e(LOG_TAG,	"Houston	we	have	a	problem..");	
												}	
												return	result;	
								}	
								@Override	
								protected	void	onPostExecute(String	result)	{	
												if(results	!=	null){	
															showResults(transformResults(result));	
												}else{	
															showResultsError();	
												}	
								}	
				}	
}
https://plus.google.com/+DianneHackborn/posts/FXCCYxepsDU
Architecture?
We have Activities,
AsyncTasks and
Services… what else do
we need?
M V P
V P M
user events updates model
updates view state change event
V P M
public class TasksFragment extends Fragment implements TasksContract.View {
private TasksContract.Presenter mPresenter;
private TasksAdapter mListAdapter;
private View mNoTasksView;
private ImageView mNoTaskIcon;
private TextView mNoTaskMainView;
private TextView mNoTaskAddView;
private LinearLayout mTasksView;
private TextView mFilteringLabelView;
...
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mListAdapter = new TasksAdapter(new ArrayList<Task>(0), mItemListener);
}
@Override
public void onResume() {
super.onResume();
mPresenter.start();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.tasks_frag, container, false);
// Set up tasks view
ListView listView = (ListView) root.findViewById(R.id.tasks_list);
listView.setAdapter(mListAdapter);
mFilteringLabelView = (TextView) root.findViewById(R.id.filteringLabel);
mTasksView = (LinearLayout) root.findViewById(R.id.tasksLL);
// Set up no tasks view
mNoTasksView = root.findViewById(R.id.noTasks);
mNoTaskIcon = (ImageView) root.findViewById(R.id.noTasksIcon);
mNoTaskMainView = (TextView) root.findViewById(R.id.noTasksMain);
mNoTaskAddView = (TextView) root.findViewById(R.id.noTasksAdd);
mNoTaskAddView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showAddTask();
}
});
...
setHasOptionsMenu(true);
return root;
}
...
public class TasksPresenter implements TasksContract.Presenter {
private final TasksRepository mTasksRepository;
private final TasksContract.View mTasksView;
private TasksFilterType mCurrentFiltering = TasksFilterType.ALL_TASKS;
private boolean mFirstLoad = true;
public TasksPresenter(@NonNull TasksRepository tasksRepository,
@NonNull TasksContract.View tasksView) {
mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
mTasksView = checkNotNull(tasksView, "tasksView cannot be null!");
mTasksView.setPresenter(this);
}
@Override
public void start() {
loadTasks(false);
}
@Override
public void result(int requestCode, int resultCode) {
// If a task was successfully added, show snackbar
if (AddEditTaskActivity.REQUEST_ADD_TASK == requestCode
&& Activity.RESULT_OK == resultCode) {
mTasksView.showSuccessfullySavedMessage();
}
}
@Override
public void loadTasks(boolean forceUpdate) {
// Simplification for sample: a network reload will be forced on first load.
loadTasks(forceUpdate || mFirstLoad, true);
mFirstLoad = false;
}
private void loadTasks(boolean forceUpdate, final boolean showLoadingUI) {
if (showLoadingUI) {
mTasksView.setLoadingIndicator(true);
}
if (forceUpdate) {
mTasksRepository.refreshTasks();
}
mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
List<Task> tasksToShow = new ArrayList<Task>()
// We filter the tasks based on the requestType
for (Task task : tasks) {
switch (mCurrentFiltering) {
case ALL_TASKS:
tasksToShow.add(task);
break;
case ACTIVE_TASKS:
public class TasksRepository implements TasksDataSource {
private static TasksRepository INSTANCE = null;
private final TasksDataSource mTasksRemoteDataSource;
private final TasksDataSource mTasksLocalDataSource;
/**
* This variable has package local visibility so it can be accessed from tests.
*/
Map<String, Task> mCachedTasks;
/**
* Returns the single instance of this class, creating it if necessary.
*
* @param tasksRemoteDataSource the backend data source
* @param tasksLocalDataSource the device storage data source
* @return the {@link TasksRepository} instance
*/
public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource,
TasksDataSource tasksLocalDataSource) {
if (INSTANCE == null) {
INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);
}
return INSTANCE;
}
/**
* Gets tasks from cache, local data source (SQLite) or remote data source, whichever is
* available first.
*/
@Override
public void getTasks(@NonNull final LoadTasksCallback callback) {
checkNotNull(callback);
// Respond immediately with cache if available and not dirty
if (mCachedTasks != null && !mCacheIsDirty) {
callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
return;
}
if (mCacheIsDirty) {
// If the cache is dirty we need to fetch new data from the network.
getTasksFromRemoteDataSource(callback);
} else {
// Query the local storage if available. If not, query the network.
mTasksLocalDataSource.getTasks(new LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
refreshCache(tasks);
callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
}
@Override
public void onDataNotAvailable() {
getTasksFromRemoteDataSource(callback);
}
public class TasksFragment extends Fragment implements TasksContract.View {
private TasksContract.Presenter mPresenter;
private TasksAdapter mListAdapter;
private View mNoTasksView;
private ImageView mNoTaskIcon;
private TextView mNoTaskMainView;
private TextView mNoTaskAddView;
private LinearLayout mTasksView;
private TextView mFilteringLabelView;
...
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mListAdapter = new TasksAdapter(new ArrayList<Task>(0), mItemListener);
}
@Override
public void onResume() {
super.onResume();
mPresenter.start();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.tasks_frag, container, false);
// Set up tasks view
ListView listView = (ListView) root.findViewById(R.id.tasks_list);
listView.setAdapter(mListAdapter);
mFilteringLabelView = (TextView) root.findViewById(R.id.filteringLabel);
mTasksView = (LinearLayout) root.findViewById(R.id.tasksLL);
// Set up no tasks view
mNoTasksView = root.findViewById(R.id.noTasks);
mNoTaskIcon = (ImageView) root.findViewById(R.id.noTasksIcon);
mNoTaskMainView = (TextView) root.findViewById(R.id.noTasksMain);
mNoTaskAddView = (TextView) root.findViewById(R.id.noTasksAdd);
mNoTaskAddView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showAddTask();
}
});
...
setHasOptionsMenu(true);
return root;
}
...
public class TasksPresenter implements TasksContract.Presenter {
private final TasksRepository mTasksRepository;
private final TasksContract.View mTasksView;
private TasksFilterType mCurrentFiltering = TasksFilterType.ALL_TASKS;
private boolean mFirstLoad = true;
public TasksPresenter(@NonNull TasksRepository tasksRepository,
@NonNull TasksContract.View tasksView) {
mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
mTasksView = checkNotNull(tasksView, "tasksView cannot be null!");
mTasksView.setPresenter(this);
}
@Override
public void start() {
loadTasks(false);
}
@Override
public void result(int requestCode, int resultCode) {
// If a task was successfully added, show snackbar
if (AddEditTaskActivity.REQUEST_ADD_TASK == requestCode
&& Activity.RESULT_OK == resultCode) {
mTasksView.showSuccessfullySavedMessage();
}
}
@Override
public void loadTasks(boolean forceUpdate) {
// Simplification for sample: a network reload will be forced on first load.
loadTasks(forceUpdate || mFirstLoad, true);
mFirstLoad = false;
}
private void loadTasks(boolean forceUpdate, final boolean showLoadingUI) {
if (showLoadingUI) {
mTasksView.setLoadingIndicator(true);
}
if (forceUpdate) {
mTasksRepository.refreshTasks();
}
mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
List<Task> tasksToShow = new ArrayList<Task>()
// We filter the tasks based on the requestType
for (Task task : tasks) {
switch (mCurrentFiltering) {
case ALL_TASKS:
tasksToShow.add(task);
break;
case ACTIVE_TASKS:
public class TasksRepository implements TasksDataSource {
private static TasksRepository INSTANCE = null;
private final TasksDataSource mTasksRemoteDataSource;
private final TasksDataSource mTasksLocalDataSource;
/**
* This variable has package local visibility so it can be accessed from tests.
*/
Map<String, Task> mCachedTasks;
/**
* Returns the single instance of this class, creating it if necessary.
*
* @param tasksRemoteDataSource the backend data source
* @param tasksLocalDataSource the device storage data source
* @return the {@link TasksRepository} instance
*/
public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource,
TasksDataSource tasksLocalDataSource) {
if (INSTANCE == null) {
INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);
}
return INSTANCE;
}
/**
* Gets tasks from cache, local data source (SQLite) or remote data source, whichever is
* available first.
*/
@Override
public void getTasks(@NonNull final LoadTasksCallback callback) {
checkNotNull(callback);
// Respond immediately with cache if available and not dirty
if (mCachedTasks != null && !mCacheIsDirty) {
callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
return;
}
if (mCacheIsDirty) {
// If the cache is dirty we need to fetch new data from the network.
getTasksFromRemoteDataSource(callback);
} else {
// Query the local storage if available. If not, query the network.
mTasksLocalDataSource.getTasks(new LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
refreshCache(tasks);
callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
}
@Override
public void onDataNotAvailable() {
getTasksFromRemoteDataSource(callback);
}
public class TasksFragment extends Fragment implements TasksContract.View {
private TasksContract.Presenter mPresenter;
private TasksAdapter mListAdapter;
private View mNoTasksView;
private ImageView mNoTaskIcon;
private TextView mNoTaskMainView;
private TextView mNoTaskAddView;
private LinearLayout mTasksView;
private TextView mFilteringLabelView;
...
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mListAdapter = new TasksAdapter(new ArrayList<Task>(0), mItemListener);
}
@Override
public void onResume() {
super.onResume();
mPresenter.start();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.tasks_frag, container, false);
// Set up tasks view
ListView listView = (ListView) root.findViewById(R.id.tasks_list);
listView.setAdapter(mListAdapter);
mFilteringLabelView = (TextView) root.findViewById(R.id.filteringLabel);
mTasksView = (LinearLayout) root.findViewById(R.id.tasksLL);
// Set up no tasks view
mNoTasksView = root.findViewById(R.id.noTasks);
mNoTaskIcon = (ImageView) root.findViewById(R.id.noTasksIcon);
mNoTaskMainView = (TextView) root.findViewById(R.id.noTasksMain);
mNoTaskAddView = (TextView) root.findViewById(R.id.noTasksAdd);
mNoTaskAddView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showAddTask();
}
});
...
setHasOptionsMenu(true);
return root;
}
...
public class TasksPresenter implements TasksContract.Presenter {
private final TasksRepository mTasksRepository;
private final TasksContract.View mTasksView;
private TasksFilterType mCurrentFiltering = TasksFilterType.ALL_TASKS;
private boolean mFirstLoad = true;
public TasksPresenter(@NonNull TasksRepository tasksRepository, @NonNull TasksContract
mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
mTasksView = checkNotNull(tasksView, "tasksView cannot be null!");
mTasksView.setPresenter(this);
}
@Override
public void start() {
loadTasks(false);
}
@Override
public void result(int requestCode, int resultCode) {
// If a task was successfully added, show snackbar
if (AddEditTaskActivity.REQUEST_ADD_TASK == requestCode && Activity.RESULT_OK == re
mTasksView.showSuccessfullySavedMessage();
}
}
@Override
public void loadTasks(boolean forceUpdate) {
// Simplification for sample: a network reload will be forced on first load.
loadTasks(forceUpdate || mFirstLoad, true);
mFirstLoad = false;
}
private void loadTasks(boolean forceUpdate, final boolean showLoadingUI) {
if (showLoadingUI) {
mTasksView.setLoadingIndicator(true);
}
if (forceUpdate) {
mTasksRepository.refreshTasks();
}
// The network request might be handled in a different thread so make sure Espresso know
// that the app is busy until the response is handled.
EspressoIdlingResource.increment(); // App is busy until further notice
mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
List<Task> tasksToShow = new ArrayList<Task>()
// We filter the tasks based on the requestType
for (Task task : tasks) {
switch (mCurrentFiltering) {
case ALL_TASKS:
tasksToShow.add(task);
break;
public class TasksRepository implements TasksDataSource {
private static TasksRepository INSTANCE = null;
private final TasksDataSource mTasksRemoteDataSource;
private final TasksDataSource mTasksLocalDataSource;
/**
* This variable has package local visibility so it can be accessed from tests.
*/
Map<String, Task> mCachedTasks;
/**
* Returns the single instance of this class, creating it if necessary.
*
* @param tasksRemoteDataSource the backend data source
* @param tasksLocalDataSource the device storage data source
* @return the {@link TasksRepository} instance
*/
public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource,
TasksDataSource tasksLocalDataSource) {
if (INSTANCE == null) {
INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);
}
return INSTANCE;
}
/**
* Gets tasks from cache, local data source (SQLite) or remote data source, whichever is
* available first.
*/
@Override
public void getTasks(@NonNull final LoadTasksCallback callback) {
checkNotNull(callback);
// Respond immediately with cache if available and not dirty
if (mCachedTasks != null && !mCacheIsDirty) {
callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
return;
}
if (mCacheIsDirty) {
// If the cache is dirty we need to fetch new data from the network.
getTasksFromRemoteDataSource(callback);
} else {
// Query the local storage if available. If not, query the network.
mTasksLocalDataSource.getTasks(new LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
refreshCache(tasks);
callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
}
@Override
public void onDataNotAvailable() {
getTasksFromRemoteDataSource(callback);
}
Consists of Model, View, and Presenter layers;
View delegates user input to Presenter;
All layers should have a 1 to 1 relation;
View and Model aren’t tightly coupled (separation of concerns);
Easy unit testing (interface for Presenter layer is easily mocked);
MVP
Done!
Any questions?
made with keynote by andrew haskin
There are still dev pains to be addressed.
1:1 dependency
Lifecycle?
State on rotation?
State on rotation?
Testability?
1:1 dependency
Lifecycle?
State on rotation?
Testability?
1:1 dependency
Lifecycle?
Do we want to?
State on rotation?
Testability?
MVP vs MVVM
1:N
class MainPresenter(private val view: MainView) {
fun getWeather() {
view.changeText("Sunny weather in Porto!”")
}
interface MainView {
fun changeText(textValue: String)
}
}
class MainActivity : AppCompatActivity(), MainPresenter.MainView {
private val presenter = MainPresenter(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
presenter.getWeather()
}
override fun changeText(textValue: String) {
weatherTextView.text = textValue
}
}
class MainViewModel {
val weatherTextValue = “Sunny weather in Porto!”
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = MainViewModel()
weatherTextView.text = viewModel. weatherTextValue
}
}
1:N
Yes you’re right, this does not solve them all
Fast Forward…
…10 years
Some assumptions…
It’s 2018.
You don’t live under a rock
You use (at least some of):
2008
2008
Android
Android is
launched
September 2008!
Android Market
Goes live October
2008
2018
Behavior
Behavior
Download Manager
Media & Playback
Permissions
Notifications
Sharing
Slices
Foundation
Foundation
AppCompat
Android KTX
Multidex
Test
UI
UI
Animation & Transisiton

Auto, TV & Wear
Emoji
Fragment
Layout
Palette
Architecture
ArchitectureData Binding
Lifecycles
LiveData
Navigation
Paging
Room
ViewModel
WorkManager
Behavior
Architecture
UI
Foundation
Behavior
Architecture
UI
Foundation
Data Binding
Lifecycles
LiveData
Navigation
Paging
Room
ViewModel
WorkManager
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModelFactory: MainViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Dependencies.inject(this)
class MainViewModel(private val service: WeatherService)
: ViewModel() {
val weatherTextValue = MutableLiveData<String>()
val inProgress = MutableLiveData<Boolean>()
private var disposable: Disposable? = null
fun onRefreshWeatherClicked() {
inProgress.value = true
disposable = service.requestWeather()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onWeatherResponse)
}
override fun onCleared() {
disposable?.dispose()
}
private fun onWeatherResponse(response: String) {
inProgress.value = false
weatherTextValue.value = response
}
}
val viewModel = viewModel.apply {
weatherTextValue.observe(this@MainActivity,
Observer { helloTextView.text = it })
inProgress.observe(this@MainActivity,
Observer { it?.let { progressBar.visibility =
if(it){ View.VISIBLE} else{ View.GONE} } })
}
button.setOnClickListener {
viewModel.onRefreshWeatherClicked() }
}
private val viewModel : MainViewModel by lazy{
ViewModelProviders.of(this, viewModelFactory)
.get(MainViewModel::class.java)
}
class MainViewModel(private val service: WeatherService)
: ViewModel() {
val weatherTextValue = MutableLiveData<String>()
val inProgress = MutableLiveData<Boolean>()
private var disposable: Disposable? = null
fun onRefreshWeatherClicked() {
inProgress.value = true
disposable = service.requestWeather()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onWeatherResponse)
}
override fun onCleared() {
disposable?.dispose()
}
private fun onWeatherResponse(response: String) {
inProgress.value = false
weatherTextValue.value = response
}
}
Progress is
changed
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModelFactory: MainViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Dependencies.inject(this)
val viewModel = viewModel.apply {
weatherTextValue.observe(this@MainActivity,
Observer { helloTextView.text = it })
inProgress.observe(this@MainActivity,
Observer { it?.let { progressBar.visibility =
if(it){ View.VISIBLE} else{ View.GONE} } })
}
button.setOnClickListener {
viewModel.onRefreshWeatherClicked() }
}
private val viewModel : MainViewModel by lazy{
ViewModelProviders.of(this, viewModelFactory)
.get(MainViewModel::class.java)
}
User clicks
button
class MainViewModel(private val service: WeatherService)
: ViewModel() {
val weatherTextValue = MutableLiveData<String>()
val inProgress = MutableLiveData<Boolean>()
private var disposable: Disposable? = null
fun onRefreshWeatherClicked() {
inProgress.value = true
disposable = service.requestWeather()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onWeatherResponse)
}
override fun onCleared() {
disposable?.dispose()
}
private fun onWeatherResponse(response: String) {
inProgress.value = false
weatherTextValue.value = response
}
}
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModelFactory: MainViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Dependencies.inject(this)
val viewModel = viewModel.apply {
weatherTextValue.observe(this@MainActivity,
Observer { helloTextView.text = it })
inProgress.observe(this@MainActivity,
Observer { it?.let { progressBar.visibility =
if(it){ View.VISIBLE} else{ View.GONE} } })
}
button.setOnClickListener {
viewModel.onRefreshWeatherClicked() }
}
private val viewModel : MainViewModel by lazy{
ViewModelProviders.of(this, viewModelFactory)
.get(MainViewModel::class.java)
}
That changes the
progress Since it’s
LiveData it’s
“pushed” to the
View
class MainViewModel(private val service: WeatherService)
: ViewModel() {
val weatherTextValue = MutableLiveData<String>()
val inProgress = MutableLiveData<Boolean>()
private var disposable: Disposable? = null
fun onRefreshWeatherClicked() {
inProgress.value = true
disposable = service.requestWeather()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onWeatherResponse)
}
override fun onCleared() {
disposable?.dispose()
}
private fun onWeatherResponse(response: String) {
inProgress.value = false
weatherTextValue.value = response
}
}
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModelFactory: MainViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Dependencies.inject(this)
val viewModel = viewModel.apply {
weatherTextValue.observe(this@MainActivity,
Observer { helloTextView.text = it })
inProgress.observe(this@MainActivity,
Observer { it?.let { progressBar.visibility =
if(it){ View.VISIBLE} else{ View.GONE} } })
}
button.setOnClickListener {
viewModel.onRefreshWeatherClicked() }
}
private val viewModel : MainViewModel by lazy{
ViewModelProviders.of(this, viewModelFactory)
.get(MainViewModel::class.java)
}
The view is
updated
The request
finishes, updates
the progress and
text, LiveData
pushes it
class MainViewModel(private val service: WeatherService)
: ViewModel() {
val weatherTextValue = MutableLiveData<String>()
val inProgress = MutableLiveData<Boolean>()
private var disposable: Disposable? = null
fun onRefreshWeatherClicked() {
inProgress.value = true
disposable = service.requestWeather()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onWeatherResponse)
}
override fun onCleared() {
disposable?.dispose()
}
private fun onWeatherResponse(response: String) {
inProgress.value = false
weatherTextValue.value = response
}
}
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModelFactory: MainViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Dependencies.inject(this)
val viewModel = viewModel.apply {
weatherTextValue.observe(this@MainActivity,
Observer { helloTextView.text = it })
inProgress.observe(this@MainActivity,
Observer { it?.let { progressBar.visibility =
if(it){ View.VISIBLE} else{ View.GONE} } })
}
button.setOnClickListener {
viewModel.onRefreshWeatherClicked() }
}
private val viewModel : MainViewModel by lazy{
ViewModelProviders.of(this, viewModelFactory)
.get(MainViewModel::class.java)
}
class MainViewModel(private val service: WeatherService)
: ViewModel() {
val weatherTextValue = MutableLiveData<String>()
val inProgress = MutableLiveData<Boolean>()
private var disposable: Disposable? = null
fun onRefreshWeatherClicked() {
inProgress.value = true
disposable = service.requestWeather()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onWeatherResponse)
}
override fun onCleared() {
disposable?.dispose()
}
private fun onWeatherResponse(response: String) {
inProgress.value = false
weatherTextValue.value = response
}
}
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModelFactory: MainViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Dependencies.inject(this)
val viewModel = viewModel.apply {
weatherTextValue.observe(this@MainActivity,
Observer { helloTextView.text = it })
inProgress.observe(this@MainActivity,
Observer { it?.let { progressBar.visibility =
if(it){ View.VISIBLE} else{ View.GONE} } })
}
button.setOnClickListener {
viewModel.onRefreshWeatherClicked() }
}
private val viewModel : MainViewModel by lazy{
ViewModelProviders.of(this, viewModelFactory)
.get(MainViewModel::class.java)
}
Also with this
magic the
ViewModel
persists on
rotation
No need for
lifecycle, just
implement this
method and 💥
class MainViewModel(private val service: WeatherService)
: ViewModel() {
val weatherTextValue = MutableLiveData<String>()
val inProgress = MutableLiveData<Boolean>()
private var disposable: Disposable? = null
fun onRefreshWeatherClicked() {
inProgress.value = true
disposable = service.requestWeather()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onWeatherResponse)
}
override fun onCleared() {
disposable?.dispose()
}
private fun onWeatherResponse(response: String) {
inProgress.value = false
weatherTextValue.value = response
}
}
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModelFactory: MainViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Dependencies.inject(this)
val viewModel = viewModel.apply {
weatherTextValue.observe(this@MainActivity,
Observer { helloTextView.text = it })
inProgress.observe(this@MainActivity,
Observer { it?.let { progressBar.visibility =
if(it){ View.VISIBLE} else{ View.GONE} } })
}
button.setOnClickListener {
viewModel.onRefreshWeatherClicked() }
}
private val viewModel : MainViewModel by lazy{
ViewModelProviders.of(this, viewModelFactory)
.get(MainViewModel::class.java)
}
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModelFactory: MainViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Dependencies.inject(this)
class MainViewModel(private val service: WeatherService)
: ViewModel() {
val weatherTextValue = MutableLiveData<String>()
val inProgress = MutableLiveData<Boolean>()
private var disposable: Disposable? = null
fun onRefreshWeatherClicked() {
inProgress.value = true
disposable = service.requestWeather()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onWeatherResponse)
}
override fun onCleared() {
disposable?.dispose()
}
private fun onWeatherResponse(response: String) {
inProgress.value = false
weatherTextValue.value = response
}
}
val binding = MainActivityBinding
.inflate(LayoutInflater.from(this)
binding.setLifecycleOwner(this)
setContentView(binding.root)
binding.button.clicks()
.subscribe { viewModel.onRefreshWeatherClicked() }
}
private val viewModel : MainViewModel by lazy{
ViewModelProviders.of(this, viewModelFactory)
.get(MainViewModel::class.java)
}
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" >
<data>
<import type="android.view.View" />
<variable name="viewModel" type="my.package.viewmodel.MainViewModel" />
</data>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintTop_toTopOf="parent"
android:text=“@{viewModel.inProgress ? View.VISIBLE : View.GONE}” />
<TextView
android:id=“@+id/weather_text_view”
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
app:layout_constraintTop_toBottomOf="@+id/progress_bar"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/button"
android:text=“@{viewModel.weatherTextValue}”/>
<Button
android:id="@+id/button"
android:layout_width="100dp"
android:layout_height=“wrap_content"
android:layout_gravity="center_horizontal"
app:layout_constraintTop_toBottomOf="@+id/progress_bar"
app:layout_constraintLeft_toRightOf="@+id/weather_text_view"
app:layout_constraintRight_toRightOf="parent"
android:text="Click me" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
</layout>
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModelFactory: MainViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Dependencies.inject(this)
val binding = MainActivityBinding
.inflate(LayoutInflater.from(this)
binding.setLifecycleOwner(this)
setContentView(binding.root)
binding.button.clicks()
.subscribe { viewModel.onRefreshWeatherClicked() }
}
private val viewModel : MainViewModel by lazy{
ViewModelProviders.of(this, viewModelFactory)
.get(MainViewModel::class.java)
}
With this you
easily get a lot of
magic UI!
class MainViewModel(private val service: WeatherService)
: ViewModel() {
val weatherTextValue = MutableLiveData<String>()
val inProgress = MutableLiveData<Boolean>()
private var disposable: Disposable? = null
fun onRefreshWeatherClicked() {
inProgress.value = true
disposable = service.requestWeather()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onWeatherResponse)
}
override fun onCleared() {
disposable?.dispose()
}
private fun onWeatherResponse(response: String) {
inProgress.value = false
weatherTextValue.value = response
}
}
What if I don’t
have network and
want to wait for it
to update screen?
Because..
!
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModelFactory: MainViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Dependencies.inject(this)
val binding = MainActivityBinding
.inflate(LayoutInflater.from(this)
binding.setLifecycleOwner(this)
setContentView(binding.root)
binding.button.clicks()
.subscribe { viewModel.onRefreshWeatherClicked() }
}
private val viewModel : MainViewModel by lazy{
ViewModelProviders.of(this, viewModelFactory)
.get(MainViewModel::class.java)
}
Behavior
Architecture
UI
Foundation
Data Binding
Lifecycles
LiveData
Navigation
Paging
Room
ViewModel
WorkManager
WorkManager
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModelFactory: MainViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Dependencies.inject(this)
val binding = MainActivityBinding
.inflate(LayoutInflater.from(this)
binding.setLifecycleOwner(this)
setContentView(binding.root)
binding.button.clicks()
.subscribe { viewModel.onRefreshWeatherClicked() }
}
private val viewModel : MainViewModel by lazy{
ViewModelProviders.of(this, viewModelFactory)
.get(MainViewModel::class.java)
}
val constraints: Constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val work: OneTimeWorkRequest = OneTimeWorkRequest
.Builder(PortugalChampionWorker::class.java)
.setConstraints(constraints).build()
WorkManager.getInstance().enqueue(work)
workId = work.id
WorkManager.getInstance().getStatusById(workId)
.observe(this, Observer { status ->
if (status?.state?.isFinished == true) {
val myResult = status.outputData.getString(KEY_RESULT,
myDefaultValue)
textValue.value = myResult
}
})
}
override fun onCleared() {
//Want to cancel when exiting screen?
workId?.let{
WorkManager.getInstance().cancelWorkById(it)
}
}
}
class PortugalChampionWorker : Worker() {
override fun doWork(): Result {
//No need for querying.. the result is clear.. success!
return Result.SUCCESS
}
}
Magic!
class MainViewModel(private val service: FootballService) : ViewModel() {
val textValue = MutableLiveData<String>()
val inProgress = MutableLiveData<Int>()
val workId : UUID? = null
private var disposable: Disposable? = null
fun onButtonClicked() {
inProgress.value = View.VISIBLE
val constraints: Constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val work: OneTimeWorkRequest = OneTimeWorkRequest
.Builder(PortugalChampionWorker::class.java)
.setConstraints(constraints).build()
WorkManager.getInstance().enqueue(work)
workId = work.id
WorkManager.getInstance().getStatusById(workId)
.observe(this, Observer { status ->
if (status != null && status.state.isFinished) {
val myResult = status.outputData.getString(KEY_RESULT,
myDefaultValue)
textValue.value = myResult
}
})
}
override fun onCleared() {
//Want to cancel when exiting screen?
workId?.let{
WorkManager.getInstance().cancelWorkById(it)
}
}
}
class PortugalChampionWorker : Worker() {
override fun doWork(): Result {
//No need for querying.. the result is clear.. success!
return Result.SUCCESS
}
}
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModelFactory: MainViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Dependencies.inject(this)
val binding = MainActivityBinding
.inflate(LayoutInflater.from(this)
binding.setLifecycleOwner(this)
setContentView(binding.root)
binding.button.clicks()
.subscribe { viewModel.onRefreshWeatherClicked() }
}
private val viewModel : MainViewModel by lazy{
ViewModelProviders.of(this, viewModelFactory)
.get(MainViewModel::class.java)
}
Hum… must finish?
You are kind of
bullshitting here,
aren’t you Pedro?
class MainViewModel(private val service: FootballService) : ViewModel() {
val textValue = MutableLiveData<String>()
val inProgress = MutableLiveData<Int>()
val workId : UUID? = null
private var disposable: Disposable? = null
fun onButtonClicked() {
inProgress.value = View.VISIBLE
But I forgive you!
Because Portugal!
Because Portugal!
Remember… to have fun!
Any questions?
made with keynote by andrew haskin
Join us at

Android Portugal Slack
http://bit.ly/AndroidPortugal
made with keynote by andrew haskin
made with keynote
Thanks to
Andrew Haskin
Carlos Mota & Renato Almeida
Kamil Seweryninspiring article
review and opinions
base slides
Don't Make Android Bad... Again

Don't Make Android Bad... Again

  • 1.
  • 2.
  • 7.
  • 9.
    Pedro Vicente Mobile SoftwareCraftsman Apps since ~2009 Self-organising company … Most beautiful person…ever HIRING
  • 13.
  • 14.
    AsyncTask ruled the(app) world! • Callback hell • Memory leaks • Spaghetti code
  • 15.
  • 16.
    From: https://labs.ribot.co.uk/android-application-architecture-8b6e34acda65 Way tomuch responsibilities: 1.Call the API and / or cache 2. Handle success or error (of both) 3.Transform the data returned from the API 4.What happens if the device rotates? 5. How to test all this? 6. And another N number of issues..
  • 17.
    From: https://labs.ribot.co.uk/android-application-architecture-8b6e34acda65 Way tomuch responsibilities: 1.Call the API and / or cache 2. Handle success or error (of both) 3.Transform the data returned from the API 4.What happens if the device rotates? 5. How to test all this? 6. And another N number of issues.. still
  • 18.
    From: https://labs.ribot.co.uk/android-application-architecture-8b6e34acda65 Way tomuch responsibilities: 1.Call the API and / or cache 2. Handle success or error (of both) 3.Transform the data returned from the API 4. How to test all this?
  • 19.
    From: https://labs.ribot.co.uk/android-application-architecture-8b6e34acda65 Way tomuch responsibilities: 1.Call the API and / or cache 2. Handle success or error (of both) 3.Transform the data returned from the API 4. How to test all this? How to get bald 101 Actually…Actually…
  • 20.
    From: https://labs.ribot.co.uk/android-application-architecture-8b6e34acda65 Way tomuch responsibilities: 1.Call the API and / or cache 2. Handle success or error (of both) 3.Transform the data returned from the API 4. How to test all this? How to get bald 101
  • 21.
  • 22.
  • 23.
  • 24.
    Architecture? We have Activities, AsyncTasksand Services… what else do we need?
  • 27.
  • 28.
    V P M userevents updates model updates view state change event
  • 29.
  • 30.
    public class TasksFragmentextends Fragment implements TasksContract.View { private TasksContract.Presenter mPresenter; private TasksAdapter mListAdapter; private View mNoTasksView; private ImageView mNoTaskIcon; private TextView mNoTaskMainView; private TextView mNoTaskAddView; private LinearLayout mTasksView; private TextView mFilteringLabelView; ... @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mListAdapter = new TasksAdapter(new ArrayList<Task>(0), mItemListener); } @Override public void onResume() { super.onResume(); mPresenter.start(); } @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.tasks_frag, container, false); // Set up tasks view ListView listView = (ListView) root.findViewById(R.id.tasks_list); listView.setAdapter(mListAdapter); mFilteringLabelView = (TextView) root.findViewById(R.id.filteringLabel); mTasksView = (LinearLayout) root.findViewById(R.id.tasksLL); // Set up no tasks view mNoTasksView = root.findViewById(R.id.noTasks); mNoTaskIcon = (ImageView) root.findViewById(R.id.noTasksIcon); mNoTaskMainView = (TextView) root.findViewById(R.id.noTasksMain); mNoTaskAddView = (TextView) root.findViewById(R.id.noTasksAdd); mNoTaskAddView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showAddTask(); } }); ... setHasOptionsMenu(true); return root; } ... public class TasksPresenter implements TasksContract.Presenter { private final TasksRepository mTasksRepository; private final TasksContract.View mTasksView; private TasksFilterType mCurrentFiltering = TasksFilterType.ALL_TASKS; private boolean mFirstLoad = true; public TasksPresenter(@NonNull TasksRepository tasksRepository, @NonNull TasksContract.View tasksView) { mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null"); mTasksView = checkNotNull(tasksView, "tasksView cannot be null!"); mTasksView.setPresenter(this); } @Override public void start() { loadTasks(false); } @Override public void result(int requestCode, int resultCode) { // If a task was successfully added, show snackbar if (AddEditTaskActivity.REQUEST_ADD_TASK == requestCode && Activity.RESULT_OK == resultCode) { mTasksView.showSuccessfullySavedMessage(); } } @Override public void loadTasks(boolean forceUpdate) { // Simplification for sample: a network reload will be forced on first load. loadTasks(forceUpdate || mFirstLoad, true); mFirstLoad = false; } private void loadTasks(boolean forceUpdate, final boolean showLoadingUI) { if (showLoadingUI) { mTasksView.setLoadingIndicator(true); } if (forceUpdate) { mTasksRepository.refreshTasks(); } mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() { @Override public void onTasksLoaded(List<Task> tasks) { List<Task> tasksToShow = new ArrayList<Task>() // We filter the tasks based on the requestType for (Task task : tasks) { switch (mCurrentFiltering) { case ALL_TASKS: tasksToShow.add(task); break; case ACTIVE_TASKS: public class TasksRepository implements TasksDataSource { private static TasksRepository INSTANCE = null; private final TasksDataSource mTasksRemoteDataSource; private final TasksDataSource mTasksLocalDataSource; /** * This variable has package local visibility so it can be accessed from tests. */ Map<String, Task> mCachedTasks; /** * Returns the single instance of this class, creating it if necessary. * * @param tasksRemoteDataSource the backend data source * @param tasksLocalDataSource the device storage data source * @return the {@link TasksRepository} instance */ public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource, TasksDataSource tasksLocalDataSource) { if (INSTANCE == null) { INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource); } return INSTANCE; } /** * Gets tasks from cache, local data source (SQLite) or remote data source, whichever is * available first. */ @Override public void getTasks(@NonNull final LoadTasksCallback callback) { checkNotNull(callback); // Respond immediately with cache if available and not dirty if (mCachedTasks != null && !mCacheIsDirty) { callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values())); return; } if (mCacheIsDirty) { // If the cache is dirty we need to fetch new data from the network. getTasksFromRemoteDataSource(callback); } else { // Query the local storage if available. If not, query the network. mTasksLocalDataSource.getTasks(new LoadTasksCallback() { @Override public void onTasksLoaded(List<Task> tasks) { refreshCache(tasks); callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values())); } @Override public void onDataNotAvailable() { getTasksFromRemoteDataSource(callback); }
  • 31.
    public class TasksFragmentextends Fragment implements TasksContract.View { private TasksContract.Presenter mPresenter; private TasksAdapter mListAdapter; private View mNoTasksView; private ImageView mNoTaskIcon; private TextView mNoTaskMainView; private TextView mNoTaskAddView; private LinearLayout mTasksView; private TextView mFilteringLabelView; ... @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mListAdapter = new TasksAdapter(new ArrayList<Task>(0), mItemListener); } @Override public void onResume() { super.onResume(); mPresenter.start(); } @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.tasks_frag, container, false); // Set up tasks view ListView listView = (ListView) root.findViewById(R.id.tasks_list); listView.setAdapter(mListAdapter); mFilteringLabelView = (TextView) root.findViewById(R.id.filteringLabel); mTasksView = (LinearLayout) root.findViewById(R.id.tasksLL); // Set up no tasks view mNoTasksView = root.findViewById(R.id.noTasks); mNoTaskIcon = (ImageView) root.findViewById(R.id.noTasksIcon); mNoTaskMainView = (TextView) root.findViewById(R.id.noTasksMain); mNoTaskAddView = (TextView) root.findViewById(R.id.noTasksAdd); mNoTaskAddView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showAddTask(); } }); ... setHasOptionsMenu(true); return root; } ... public class TasksPresenter implements TasksContract.Presenter { private final TasksRepository mTasksRepository; private final TasksContract.View mTasksView; private TasksFilterType mCurrentFiltering = TasksFilterType.ALL_TASKS; private boolean mFirstLoad = true; public TasksPresenter(@NonNull TasksRepository tasksRepository, @NonNull TasksContract.View tasksView) { mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null"); mTasksView = checkNotNull(tasksView, "tasksView cannot be null!"); mTasksView.setPresenter(this); } @Override public void start() { loadTasks(false); } @Override public void result(int requestCode, int resultCode) { // If a task was successfully added, show snackbar if (AddEditTaskActivity.REQUEST_ADD_TASK == requestCode && Activity.RESULT_OK == resultCode) { mTasksView.showSuccessfullySavedMessage(); } } @Override public void loadTasks(boolean forceUpdate) { // Simplification for sample: a network reload will be forced on first load. loadTasks(forceUpdate || mFirstLoad, true); mFirstLoad = false; } private void loadTasks(boolean forceUpdate, final boolean showLoadingUI) { if (showLoadingUI) { mTasksView.setLoadingIndicator(true); } if (forceUpdate) { mTasksRepository.refreshTasks(); } mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() { @Override public void onTasksLoaded(List<Task> tasks) { List<Task> tasksToShow = new ArrayList<Task>() // We filter the tasks based on the requestType for (Task task : tasks) { switch (mCurrentFiltering) { case ALL_TASKS: tasksToShow.add(task); break; case ACTIVE_TASKS: public class TasksRepository implements TasksDataSource { private static TasksRepository INSTANCE = null; private final TasksDataSource mTasksRemoteDataSource; private final TasksDataSource mTasksLocalDataSource; /** * This variable has package local visibility so it can be accessed from tests. */ Map<String, Task> mCachedTasks; /** * Returns the single instance of this class, creating it if necessary. * * @param tasksRemoteDataSource the backend data source * @param tasksLocalDataSource the device storage data source * @return the {@link TasksRepository} instance */ public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource, TasksDataSource tasksLocalDataSource) { if (INSTANCE == null) { INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource); } return INSTANCE; } /** * Gets tasks from cache, local data source (SQLite) or remote data source, whichever is * available first. */ @Override public void getTasks(@NonNull final LoadTasksCallback callback) { checkNotNull(callback); // Respond immediately with cache if available and not dirty if (mCachedTasks != null && !mCacheIsDirty) { callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values())); return; } if (mCacheIsDirty) { // If the cache is dirty we need to fetch new data from the network. getTasksFromRemoteDataSource(callback); } else { // Query the local storage if available. If not, query the network. mTasksLocalDataSource.getTasks(new LoadTasksCallback() { @Override public void onTasksLoaded(List<Task> tasks) { refreshCache(tasks); callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values())); } @Override public void onDataNotAvailable() { getTasksFromRemoteDataSource(callback); }
  • 32.
    public class TasksFragmentextends Fragment implements TasksContract.View { private TasksContract.Presenter mPresenter; private TasksAdapter mListAdapter; private View mNoTasksView; private ImageView mNoTaskIcon; private TextView mNoTaskMainView; private TextView mNoTaskAddView; private LinearLayout mTasksView; private TextView mFilteringLabelView; ... @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mListAdapter = new TasksAdapter(new ArrayList<Task>(0), mItemListener); } @Override public void onResume() { super.onResume(); mPresenter.start(); } @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.tasks_frag, container, false); // Set up tasks view ListView listView = (ListView) root.findViewById(R.id.tasks_list); listView.setAdapter(mListAdapter); mFilteringLabelView = (TextView) root.findViewById(R.id.filteringLabel); mTasksView = (LinearLayout) root.findViewById(R.id.tasksLL); // Set up no tasks view mNoTasksView = root.findViewById(R.id.noTasks); mNoTaskIcon = (ImageView) root.findViewById(R.id.noTasksIcon); mNoTaskMainView = (TextView) root.findViewById(R.id.noTasksMain); mNoTaskAddView = (TextView) root.findViewById(R.id.noTasksAdd); mNoTaskAddView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showAddTask(); } }); ... setHasOptionsMenu(true); return root; } ... public class TasksPresenter implements TasksContract.Presenter { private final TasksRepository mTasksRepository; private final TasksContract.View mTasksView; private TasksFilterType mCurrentFiltering = TasksFilterType.ALL_TASKS; private boolean mFirstLoad = true; public TasksPresenter(@NonNull TasksRepository tasksRepository, @NonNull TasksContract mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null"); mTasksView = checkNotNull(tasksView, "tasksView cannot be null!"); mTasksView.setPresenter(this); } @Override public void start() { loadTasks(false); } @Override public void result(int requestCode, int resultCode) { // If a task was successfully added, show snackbar if (AddEditTaskActivity.REQUEST_ADD_TASK == requestCode && Activity.RESULT_OK == re mTasksView.showSuccessfullySavedMessage(); } } @Override public void loadTasks(boolean forceUpdate) { // Simplification for sample: a network reload will be forced on first load. loadTasks(forceUpdate || mFirstLoad, true); mFirstLoad = false; } private void loadTasks(boolean forceUpdate, final boolean showLoadingUI) { if (showLoadingUI) { mTasksView.setLoadingIndicator(true); } if (forceUpdate) { mTasksRepository.refreshTasks(); } // The network request might be handled in a different thread so make sure Espresso know // that the app is busy until the response is handled. EspressoIdlingResource.increment(); // App is busy until further notice mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() { @Override public void onTasksLoaded(List<Task> tasks) { List<Task> tasksToShow = new ArrayList<Task>() // We filter the tasks based on the requestType for (Task task : tasks) { switch (mCurrentFiltering) { case ALL_TASKS: tasksToShow.add(task); break; public class TasksRepository implements TasksDataSource { private static TasksRepository INSTANCE = null; private final TasksDataSource mTasksRemoteDataSource; private final TasksDataSource mTasksLocalDataSource; /** * This variable has package local visibility so it can be accessed from tests. */ Map<String, Task> mCachedTasks; /** * Returns the single instance of this class, creating it if necessary. * * @param tasksRemoteDataSource the backend data source * @param tasksLocalDataSource the device storage data source * @return the {@link TasksRepository} instance */ public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource, TasksDataSource tasksLocalDataSource) { if (INSTANCE == null) { INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource); } return INSTANCE; } /** * Gets tasks from cache, local data source (SQLite) or remote data source, whichever is * available first. */ @Override public void getTasks(@NonNull final LoadTasksCallback callback) { checkNotNull(callback); // Respond immediately with cache if available and not dirty if (mCachedTasks != null && !mCacheIsDirty) { callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values())); return; } if (mCacheIsDirty) { // If the cache is dirty we need to fetch new data from the network. getTasksFromRemoteDataSource(callback); } else { // Query the local storage if available. If not, query the network. mTasksLocalDataSource.getTasks(new LoadTasksCallback() { @Override public void onTasksLoaded(List<Task> tasks) { refreshCache(tasks); callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values())); } @Override public void onDataNotAvailable() { getTasksFromRemoteDataSource(callback); }
  • 33.
    Consists of Model,View, and Presenter layers; View delegates user input to Presenter; All layers should have a 1 to 1 relation; View and Model aren’t tightly coupled (separation of concerns); Easy unit testing (interface for Presenter layer is easily mocked); MVP
  • 34.
    Done! Any questions? made withkeynote by andrew haskin
  • 36.
    There are stilldev pains to be addressed.
  • 38.
    1:1 dependency Lifecycle? State onrotation? State on rotation? Testability?
  • 39.
    1:1 dependency Lifecycle? State onrotation? Testability?
  • 40.
    1:1 dependency Lifecycle? Do wewant to? State on rotation? Testability?
  • 41.
  • 42.
  • 43.
    class MainPresenter(private valview: MainView) { fun getWeather() { view.changeText("Sunny weather in Porto!”") } interface MainView { fun changeText(textValue: String) } } class MainActivity : AppCompatActivity(), MainPresenter.MainView { private val presenter = MainPresenter(this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) presenter.getWeather() } override fun changeText(textValue: String) { weatherTextView.text = textValue } } class MainViewModel { val weatherTextValue = “Sunny weather in Porto!” } class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val viewModel = MainViewModel() weatherTextView.text = viewModel. weatherTextValue } }
  • 44.
    1:N Yes you’re right,this does not solve them all
  • 46.
  • 47.
    Some assumptions… It’s 2018. Youdon’t live under a rock You use (at least some of):
  • 48.
  • 54.
  • 55.
    Behavior Download Manager Media &Playback Permissions Notifications Sharing Slices
  • 56.
  • 57.
  • 58.
  • 59.
    UI Animation & Transisiton
 Auto,TV & Wear Emoji Fragment Layout Palette
  • 60.
  • 61.
  • 62.
  • 63.
  • 70.
    class MainActivity :AppCompatActivity() { @Inject lateinit var viewModelFactory: MainViewModelFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Dependencies.inject(this) class MainViewModel(private val service: WeatherService) : ViewModel() { val weatherTextValue = MutableLiveData<String>() val inProgress = MutableLiveData<Boolean>() private var disposable: Disposable? = null fun onRefreshWeatherClicked() { inProgress.value = true disposable = service.requestWeather() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::onWeatherResponse) } override fun onCleared() { disposable?.dispose() } private fun onWeatherResponse(response: String) { inProgress.value = false weatherTextValue.value = response } } val viewModel = viewModel.apply { weatherTextValue.observe(this@MainActivity, Observer { helloTextView.text = it }) inProgress.observe(this@MainActivity, Observer { it?.let { progressBar.visibility = if(it){ View.VISIBLE} else{ View.GONE} } }) } button.setOnClickListener { viewModel.onRefreshWeatherClicked() } } private val viewModel : MainViewModel by lazy{ ViewModelProviders.of(this, viewModelFactory) .get(MainViewModel::class.java) }
  • 71.
    class MainViewModel(private valservice: WeatherService) : ViewModel() { val weatherTextValue = MutableLiveData<String>() val inProgress = MutableLiveData<Boolean>() private var disposable: Disposable? = null fun onRefreshWeatherClicked() { inProgress.value = true disposable = service.requestWeather() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::onWeatherResponse) } override fun onCleared() { disposable?.dispose() } private fun onWeatherResponse(response: String) { inProgress.value = false weatherTextValue.value = response } } Progress is changed class MainActivity : AppCompatActivity() { @Inject lateinit var viewModelFactory: MainViewModelFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Dependencies.inject(this) val viewModel = viewModel.apply { weatherTextValue.observe(this@MainActivity, Observer { helloTextView.text = it }) inProgress.observe(this@MainActivity, Observer { it?.let { progressBar.visibility = if(it){ View.VISIBLE} else{ View.GONE} } }) } button.setOnClickListener { viewModel.onRefreshWeatherClicked() } } private val viewModel : MainViewModel by lazy{ ViewModelProviders.of(this, viewModelFactory) .get(MainViewModel::class.java) } User clicks button
  • 72.
    class MainViewModel(private valservice: WeatherService) : ViewModel() { val weatherTextValue = MutableLiveData<String>() val inProgress = MutableLiveData<Boolean>() private var disposable: Disposable? = null fun onRefreshWeatherClicked() { inProgress.value = true disposable = service.requestWeather() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::onWeatherResponse) } override fun onCleared() { disposable?.dispose() } private fun onWeatherResponse(response: String) { inProgress.value = false weatherTextValue.value = response } } class MainActivity : AppCompatActivity() { @Inject lateinit var viewModelFactory: MainViewModelFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Dependencies.inject(this) val viewModel = viewModel.apply { weatherTextValue.observe(this@MainActivity, Observer { helloTextView.text = it }) inProgress.observe(this@MainActivity, Observer { it?.let { progressBar.visibility = if(it){ View.VISIBLE} else{ View.GONE} } }) } button.setOnClickListener { viewModel.onRefreshWeatherClicked() } } private val viewModel : MainViewModel by lazy{ ViewModelProviders.of(this, viewModelFactory) .get(MainViewModel::class.java) } That changes the progress Since it’s LiveData it’s “pushed” to the View
  • 73.
    class MainViewModel(private valservice: WeatherService) : ViewModel() { val weatherTextValue = MutableLiveData<String>() val inProgress = MutableLiveData<Boolean>() private var disposable: Disposable? = null fun onRefreshWeatherClicked() { inProgress.value = true disposable = service.requestWeather() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::onWeatherResponse) } override fun onCleared() { disposable?.dispose() } private fun onWeatherResponse(response: String) { inProgress.value = false weatherTextValue.value = response } } class MainActivity : AppCompatActivity() { @Inject lateinit var viewModelFactory: MainViewModelFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Dependencies.inject(this) val viewModel = viewModel.apply { weatherTextValue.observe(this@MainActivity, Observer { helloTextView.text = it }) inProgress.observe(this@MainActivity, Observer { it?.let { progressBar.visibility = if(it){ View.VISIBLE} else{ View.GONE} } }) } button.setOnClickListener { viewModel.onRefreshWeatherClicked() } } private val viewModel : MainViewModel by lazy{ ViewModelProviders.of(this, viewModelFactory) .get(MainViewModel::class.java) } The view is updated The request finishes, updates the progress and text, LiveData pushes it
  • 74.
    class MainViewModel(private valservice: WeatherService) : ViewModel() { val weatherTextValue = MutableLiveData<String>() val inProgress = MutableLiveData<Boolean>() private var disposable: Disposable? = null fun onRefreshWeatherClicked() { inProgress.value = true disposable = service.requestWeather() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::onWeatherResponse) } override fun onCleared() { disposable?.dispose() } private fun onWeatherResponse(response: String) { inProgress.value = false weatherTextValue.value = response } } class MainActivity : AppCompatActivity() { @Inject lateinit var viewModelFactory: MainViewModelFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Dependencies.inject(this) val viewModel = viewModel.apply { weatherTextValue.observe(this@MainActivity, Observer { helloTextView.text = it }) inProgress.observe(this@MainActivity, Observer { it?.let { progressBar.visibility = if(it){ View.VISIBLE} else{ View.GONE} } }) } button.setOnClickListener { viewModel.onRefreshWeatherClicked() } } private val viewModel : MainViewModel by lazy{ ViewModelProviders.of(this, viewModelFactory) .get(MainViewModel::class.java) }
  • 75.
    class MainViewModel(private valservice: WeatherService) : ViewModel() { val weatherTextValue = MutableLiveData<String>() val inProgress = MutableLiveData<Boolean>() private var disposable: Disposable? = null fun onRefreshWeatherClicked() { inProgress.value = true disposable = service.requestWeather() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::onWeatherResponse) } override fun onCleared() { disposable?.dispose() } private fun onWeatherResponse(response: String) { inProgress.value = false weatherTextValue.value = response } } class MainActivity : AppCompatActivity() { @Inject lateinit var viewModelFactory: MainViewModelFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Dependencies.inject(this) val viewModel = viewModel.apply { weatherTextValue.observe(this@MainActivity, Observer { helloTextView.text = it }) inProgress.observe(this@MainActivity, Observer { it?.let { progressBar.visibility = if(it){ View.VISIBLE} else{ View.GONE} } }) } button.setOnClickListener { viewModel.onRefreshWeatherClicked() } } private val viewModel : MainViewModel by lazy{ ViewModelProviders.of(this, viewModelFactory) .get(MainViewModel::class.java) } Also with this magic the ViewModel persists on rotation No need for lifecycle, just implement this method and 💥
  • 76.
    class MainViewModel(private valservice: WeatherService) : ViewModel() { val weatherTextValue = MutableLiveData<String>() val inProgress = MutableLiveData<Boolean>() private var disposable: Disposable? = null fun onRefreshWeatherClicked() { inProgress.value = true disposable = service.requestWeather() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::onWeatherResponse) } override fun onCleared() { disposable?.dispose() } private fun onWeatherResponse(response: String) { inProgress.value = false weatherTextValue.value = response } } class MainActivity : AppCompatActivity() { @Inject lateinit var viewModelFactory: MainViewModelFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Dependencies.inject(this) val viewModel = viewModel.apply { weatherTextValue.observe(this@MainActivity, Observer { helloTextView.text = it }) inProgress.observe(this@MainActivity, Observer { it?.let { progressBar.visibility = if(it){ View.VISIBLE} else{ View.GONE} } }) } button.setOnClickListener { viewModel.onRefreshWeatherClicked() } } private val viewModel : MainViewModel by lazy{ ViewModelProviders.of(this, viewModelFactory) .get(MainViewModel::class.java) }
  • 77.
    class MainActivity :AppCompatActivity() { @Inject lateinit var viewModelFactory: MainViewModelFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Dependencies.inject(this) class MainViewModel(private val service: WeatherService) : ViewModel() { val weatherTextValue = MutableLiveData<String>() val inProgress = MutableLiveData<Boolean>() private var disposable: Disposable? = null fun onRefreshWeatherClicked() { inProgress.value = true disposable = service.requestWeather() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::onWeatherResponse) } override fun onCleared() { disposable?.dispose() } private fun onWeatherResponse(response: String) { inProgress.value = false weatherTextValue.value = response } } val binding = MainActivityBinding .inflate(LayoutInflater.from(this) binding.setLifecycleOwner(this) setContentView(binding.root) binding.button.clicks() .subscribe { viewModel.onRefreshWeatherClicked() } } private val viewModel : MainViewModel by lazy{ ViewModelProviders.of(this, viewModelFactory) .get(MainViewModel::class.java) }
  • 79.
    <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" > <data> <importtype="android.view.View" /> <variable name="viewModel" type="my.package.viewmodel.MainViewModel" /> </data> <android.support.v7.widget.CardView android:layout_width="match_parent" android:layout_height="wrap_content"> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <ProgressBar android:id="@+id/progress_bar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:indeterminate="true" app:layout_constraintTop_toTopOf="parent" android:text=“@{viewModel.inProgress ? View.VISIBLE : View.GONE}” /> <TextView android:id=“@+id/weather_text_view” android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" app:layout_constraintTop_toBottomOf="@+id/progress_bar" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/button" android:text=“@{viewModel.weatherTextValue}”/> <Button android:id="@+id/button" android:layout_width="100dp" android:layout_height=“wrap_content" android:layout_gravity="center_horizontal" app:layout_constraintTop_toBottomOf="@+id/progress_bar" app:layout_constraintLeft_toRightOf="@+id/weather_text_view" app:layout_constraintRight_toRightOf="parent" android:text="Click me" /> </android.support.constraint.ConstraintLayout> </android.support.v7.widget.CardView> </layout> class MainActivity : AppCompatActivity() { @Inject lateinit var viewModelFactory: MainViewModelFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Dependencies.inject(this) val binding = MainActivityBinding .inflate(LayoutInflater.from(this) binding.setLifecycleOwner(this) setContentView(binding.root) binding.button.clicks() .subscribe { viewModel.onRefreshWeatherClicked() } } private val viewModel : MainViewModel by lazy{ ViewModelProviders.of(this, viewModelFactory) .get(MainViewModel::class.java) }
  • 82.
    With this you easilyget a lot of magic UI!
  • 90.
    class MainViewModel(private valservice: WeatherService) : ViewModel() { val weatherTextValue = MutableLiveData<String>() val inProgress = MutableLiveData<Boolean>() private var disposable: Disposable? = null fun onRefreshWeatherClicked() { inProgress.value = true disposable = service.requestWeather() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::onWeatherResponse) } override fun onCleared() { disposable?.dispose() } private fun onWeatherResponse(response: String) { inProgress.value = false weatherTextValue.value = response } } What if I don’t have network and want to wait for it to update screen? Because.. ! class MainActivity : AppCompatActivity() { @Inject lateinit var viewModelFactory: MainViewModelFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Dependencies.inject(this) val binding = MainActivityBinding .inflate(LayoutInflater.from(this) binding.setLifecycleOwner(this) setContentView(binding.root) binding.button.clicks() .subscribe { viewModel.onRefreshWeatherClicked() } } private val viewModel : MainViewModel by lazy{ ViewModelProviders.of(this, viewModelFactory) .get(MainViewModel::class.java) }
  • 91.
  • 92.
  • 93.
    class MainActivity :AppCompatActivity() { @Inject lateinit var viewModelFactory: MainViewModelFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Dependencies.inject(this) val binding = MainActivityBinding .inflate(LayoutInflater.from(this) binding.setLifecycleOwner(this) setContentView(binding.root) binding.button.clicks() .subscribe { viewModel.onRefreshWeatherClicked() } } private val viewModel : MainViewModel by lazy{ ViewModelProviders.of(this, viewModelFactory) .get(MainViewModel::class.java) } val constraints: Constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val work: OneTimeWorkRequest = OneTimeWorkRequest .Builder(PortugalChampionWorker::class.java) .setConstraints(constraints).build() WorkManager.getInstance().enqueue(work) workId = work.id WorkManager.getInstance().getStatusById(workId) .observe(this, Observer { status -> if (status?.state?.isFinished == true) { val myResult = status.outputData.getString(KEY_RESULT, myDefaultValue) textValue.value = myResult } }) } override fun onCleared() { //Want to cancel when exiting screen? workId?.let{ WorkManager.getInstance().cancelWorkById(it) } } } class PortugalChampionWorker : Worker() { override fun doWork(): Result { //No need for querying.. the result is clear.. success! return Result.SUCCESS } } Magic! class MainViewModel(private val service: FootballService) : ViewModel() { val textValue = MutableLiveData<String>() val inProgress = MutableLiveData<Int>() val workId : UUID? = null private var disposable: Disposable? = null fun onButtonClicked() { inProgress.value = View.VISIBLE
  • 94.
    val constraints: Constraints= Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val work: OneTimeWorkRequest = OneTimeWorkRequest .Builder(PortugalChampionWorker::class.java) .setConstraints(constraints).build() WorkManager.getInstance().enqueue(work) workId = work.id WorkManager.getInstance().getStatusById(workId) .observe(this, Observer { status -> if (status != null && status.state.isFinished) { val myResult = status.outputData.getString(KEY_RESULT, myDefaultValue) textValue.value = myResult } }) } override fun onCleared() { //Want to cancel when exiting screen? workId?.let{ WorkManager.getInstance().cancelWorkById(it) } } } class PortugalChampionWorker : Worker() { override fun doWork(): Result { //No need for querying.. the result is clear.. success! return Result.SUCCESS } } class MainActivity : AppCompatActivity() { @Inject lateinit var viewModelFactory: MainViewModelFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Dependencies.inject(this) val binding = MainActivityBinding .inflate(LayoutInflater.from(this) binding.setLifecycleOwner(this) setContentView(binding.root) binding.button.clicks() .subscribe { viewModel.onRefreshWeatherClicked() } } private val viewModel : MainViewModel by lazy{ ViewModelProviders.of(this, viewModelFactory) .get(MainViewModel::class.java) } Hum… must finish? You are kind of bullshitting here, aren’t you Pedro? class MainViewModel(private val service: FootballService) : ViewModel() { val textValue = MutableLiveData<String>() val inProgress = MutableLiveData<Int>() val workId : UUID? = null private var disposable: Disposable? = null fun onButtonClicked() { inProgress.value = View.VISIBLE But I forgive you!
  • 95.
  • 96.
  • 97.
  • 98.
    Any questions? made withkeynote by andrew haskin
  • 99.
    Join us at
 AndroidPortugal Slack http://bit.ly/AndroidPortugal made with keynote by andrew haskin
  • 100.
    made with keynote Thanksto Andrew Haskin Carlos Mota & Renato Almeida Kamil Seweryninspiring article review and opinions base slides