SlideShare a Scribd company logo
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

More Related Content

What's hot

Android programming -_pushing_the_limits
Android programming -_pushing_the_limitsAndroid programming -_pushing_the_limits
Android programming -_pushing_the_limits
Droidcon Berlin
 
Java 5 6 Generics, Concurrency, Garbage Collection, Tuning
Java 5 6 Generics, Concurrency, Garbage Collection, TuningJava 5 6 Generics, Concurrency, Garbage Collection, Tuning
Java 5 6 Generics, Concurrency, Garbage Collection, Tuning
Carol McDonald
 
Executing Sql Commands
Executing Sql CommandsExecuting Sql Commands
Executing Sql Commands
leminhvuong
 
Advanced Hibernate
Advanced HibernateAdvanced Hibernate
Advanced Hibernate
Haitham Raik
 
JavaScript Proven Practises
JavaScript Proven PractisesJavaScript Proven Practises
JavaScript Proven Practises
Robert MacLean
 
Advanced Hibernate V2
Advanced Hibernate V2Advanced Hibernate V2
Advanced Hibernate V2
Haitham Raik
 
Java Bytecode for Discriminating Developers - JavaZone 2011
Java Bytecode for Discriminating Developers - JavaZone 2011Java Bytecode for Discriminating Developers - JavaZone 2011
Java Bytecode for Discriminating Developers - JavaZone 2011
Anton Arhipov
 
Fixing the Java Serialization Mess
Fixing the Java Serialization Mess Fixing the Java Serialization Mess
Fixing the Java Serialization Mess
Salesforce Engineering
 

What's hot (20)

Java
JavaJava
Java
 
Java Concurrency
Java ConcurrencyJava Concurrency
Java Concurrency
 
Android programming -_pushing_the_limits
Android programming -_pushing_the_limitsAndroid programming -_pushing_the_limits
Android programming -_pushing_the_limits
 
Beautiful java script
Beautiful java scriptBeautiful java script
Beautiful java script
 
Advanced JavaScript Concepts
Advanced JavaScript ConceptsAdvanced JavaScript Concepts
Advanced JavaScript Concepts
 
Managing parallelism using coroutines
Managing parallelism using coroutinesManaging parallelism using coroutines
Managing parallelism using coroutines
 
Java 5 6 Generics, Concurrency, Garbage Collection, Tuning
Java 5 6 Generics, Concurrency, Garbage Collection, TuningJava 5 6 Generics, Concurrency, Garbage Collection, Tuning
Java 5 6 Generics, Concurrency, Garbage Collection, Tuning
 
Executing Sql Commands
Executing Sql CommandsExecuting Sql Commands
Executing Sql Commands
 
How to build to do app using vue composition api and vuex 4 with typescript
How to build to do app using vue composition api and vuex 4 with typescriptHow to build to do app using vue composition api and vuex 4 with typescript
How to build to do app using vue composition api and vuex 4 with typescript
 
Call Back
Call BackCall Back
Call Back
 
Advanced javascript
Advanced javascriptAdvanced javascript
Advanced javascript
 
OWASP SD: Deserialize My Shorts: Or How I Learned To Start Worrying and Hate ...
OWASP SD: Deserialize My Shorts: Or How I Learned To Start Worrying and Hate ...OWASP SD: Deserialize My Shorts: Or How I Learned To Start Worrying and Hate ...
OWASP SD: Deserialize My Shorts: Or How I Learned To Start Worrying and Hate ...
 
Advanced Hibernate
Advanced HibernateAdvanced Hibernate
Advanced Hibernate
 
50 new features of Java EE 7 in 50 minutes
50 new features of Java EE 7 in 50 minutes50 new features of Java EE 7 in 50 minutes
50 new features of Java EE 7 in 50 minutes
 
JavaScript Proven Practises
JavaScript Proven PractisesJavaScript Proven Practises
JavaScript Proven Practises
 
Making Java more dynamic: runtime code generation for the JVM
Making Java more dynamic: runtime code generation for the JVMMaking Java more dynamic: runtime code generation for the JVM
Making Java more dynamic: runtime code generation for the JVM
 
Advanced Hibernate V2
Advanced Hibernate V2Advanced Hibernate V2
Advanced Hibernate V2
 
Java Bytecode for Discriminating Developers - JavaZone 2011
Java Bytecode for Discriminating Developers - JavaZone 2011Java Bytecode for Discriminating Developers - JavaZone 2011
Java Bytecode for Discriminating Developers - JavaZone 2011
 
Javantura v2 - All Together Now - making Groovy and Scala sing together - Din...
Javantura v2 - All Together Now - making Groovy and Scala sing together - Din...Javantura v2 - All Together Now - making Groovy and Scala sing together - Din...
Javantura v2 - All Together Now - making Groovy and Scala sing together - Din...
 
Fixing the Java Serialization Mess
Fixing the Java Serialization Mess Fixing the Java Serialization Mess
Fixing the Java Serialization Mess
 

Similar to Don't Make Android Bad... Again

Powerful persistence layer with Google Guice & MyBatis
Powerful persistence layer with Google Guice & MyBatisPowerful persistence layer with Google Guice & MyBatis
Powerful persistence layer with Google Guice & MyBatis
simonetripodi
 
Overview of Android Infrastructure
Overview of Android InfrastructureOverview of Android Infrastructure
Overview of Android Infrastructure
C.T.Co
 
Overview of Android Infrastructure
Overview of Android InfrastructureOverview of Android Infrastructure
Overview of Android Infrastructure
Alexey Buzdin
 
Droidcon2013 android experience lahoda
Droidcon2013 android experience lahodaDroidcon2013 android experience lahoda
Droidcon2013 android experience lahoda
Droidcon Berlin
 

Similar to Don't Make Android Bad... Again (20)

Modern Android app library stack
Modern Android app library stackModern Android app library stack
Modern Android app library stack
 
比XML更好用的Java Annotation
比XML更好用的Java Annotation比XML更好用的Java Annotation
比XML更好用的Java Annotation
 
Architecture components - IT Talk
Architecture components - IT TalkArchitecture components - IT Talk
Architecture components - IT Talk
 
Architecture Components
Architecture Components Architecture Components
Architecture Components
 
Android architecture blueprints overview
Android architecture blueprints overviewAndroid architecture blueprints overview
Android architecture blueprints overview
 
Android workshop
Android workshopAndroid workshop
Android workshop
 
Real Life Clean Architecture
Real Life Clean ArchitectureReal Life Clean Architecture
Real Life Clean Architecture
 
From Legacy to Hexagonal (An Unexpected Android Journey)
From Legacy to Hexagonal (An Unexpected Android Journey)From Legacy to Hexagonal (An Unexpected Android Journey)
From Legacy to Hexagonal (An Unexpected Android Journey)
 
Using the Windows 8 Runtime from C++
Using the Windows 8 Runtime from C++Using the Windows 8 Runtime from C++
Using the Windows 8 Runtime from C++
 
Advance Java Programs skeleton
Advance Java Programs skeletonAdvance Java Programs skeleton
Advance Java Programs skeleton
 
Powerful persistence layer with Google Guice & MyBatis
Powerful persistence layer with Google Guice & MyBatisPowerful persistence layer with Google Guice & MyBatis
Powerful persistence layer with Google Guice & MyBatis
 
Overview of Android Infrastructure
Overview of Android InfrastructureOverview of Android Infrastructure
Overview of Android Infrastructure
 
Overview of Android Infrastructure
Overview of Android InfrastructureOverview of Android Infrastructure
Overview of Android Infrastructure
 
Taming Core Data by Arek Holko, Macoscope
Taming Core Data by Arek Holko, MacoscopeTaming Core Data by Arek Holko, Macoscope
Taming Core Data by Arek Holko, Macoscope
 
Slightly Advanced Android Wear ;)
Slightly Advanced Android Wear ;)Slightly Advanced Android Wear ;)
Slightly Advanced Android Wear ;)
 
Oleksandr Tolstykh
Oleksandr TolstykhOleksandr Tolstykh
Oleksandr Tolstykh
 
Arquitecturas de microservicios - Medianet Software
Arquitecturas de microservicios   -  Medianet SoftwareArquitecturas de microservicios   -  Medianet Software
Arquitecturas de microservicios - Medianet Software
 
Improving android experience for both users and developers
Improving android experience for both users and developersImproving android experience for both users and developers
Improving android experience for both users and developers
 
Droidcon2013 android experience lahoda
Droidcon2013 android experience lahodaDroidcon2013 android experience lahoda
Droidcon2013 android experience lahoda
 
Unittests für Dummies
Unittests für DummiesUnittests für Dummies
Unittests für Dummies
 

Recently uploaded

Structuring Teams and Portfolios for Success
Structuring Teams and Portfolios for SuccessStructuring Teams and Portfolios for Success
Structuring Teams and Portfolios for Success
UXDXConf
 

Recently uploaded (20)

Exploring UiPath Orchestrator API: updates and limits in 2024 🚀
Exploring UiPath Orchestrator API: updates and limits in 2024 🚀Exploring UiPath Orchestrator API: updates and limits in 2024 🚀
Exploring UiPath Orchestrator API: updates and limits in 2024 🚀
 
Speed Wins: From Kafka to APIs in Minutes
Speed Wins: From Kafka to APIs in MinutesSpeed Wins: From Kafka to APIs in Minutes
Speed Wins: From Kafka to APIs in Minutes
 
WSO2CONMay2024OpenSourceConferenceDebrief.pptx
WSO2CONMay2024OpenSourceConferenceDebrief.pptxWSO2CONMay2024OpenSourceConferenceDebrief.pptx
WSO2CONMay2024OpenSourceConferenceDebrief.pptx
 
SOQL 201 for Admins & Developers: Slice & Dice Your Org’s Data With Aggregate...
SOQL 201 for Admins & Developers: Slice & Dice Your Org’s Data With Aggregate...SOQL 201 for Admins & Developers: Slice & Dice Your Org’s Data With Aggregate...
SOQL 201 for Admins & Developers: Slice & Dice Your Org’s Data With Aggregate...
 
AI revolution and Salesforce, Jiří Karpíšek
AI revolution and Salesforce, Jiří KarpíšekAI revolution and Salesforce, Jiří Karpíšek
AI revolution and Salesforce, Jiří Karpíšek
 
To Graph or Not to Graph Knowledge Graph Architectures and LLMs
To Graph or Not to Graph Knowledge Graph Architectures and LLMsTo Graph or Not to Graph Knowledge Graph Architectures and LLMs
To Graph or Not to Graph Knowledge Graph Architectures and LLMs
 
The architecture of Generative AI for enterprises.pdf
The architecture of Generative AI for enterprises.pdfThe architecture of Generative AI for enterprises.pdf
The architecture of Generative AI for enterprises.pdf
 
Connector Corner: Automate dynamic content and events by pushing a button
Connector Corner: Automate dynamic content and events by pushing a buttonConnector Corner: Automate dynamic content and events by pushing a button
Connector Corner: Automate dynamic content and events by pushing a button
 
Software Delivery At the Speed of AI: Inflectra Invests In AI-Powered Quality
Software Delivery At the Speed of AI: Inflectra Invests In AI-Powered QualitySoftware Delivery At the Speed of AI: Inflectra Invests In AI-Powered Quality
Software Delivery At the Speed of AI: Inflectra Invests In AI-Powered Quality
 
Salesforce Adoption – Metrics, Methods, and Motivation, Antone Kom
Salesforce Adoption – Metrics, Methods, and Motivation, Antone KomSalesforce Adoption – Metrics, Methods, and Motivation, Antone Kom
Salesforce Adoption – Metrics, Methods, and Motivation, Antone Kom
 
Knowledge engineering: from people to machines and back
Knowledge engineering: from people to machines and backKnowledge engineering: from people to machines and back
Knowledge engineering: from people to machines and back
 
JMeter webinar - integration with InfluxDB and Grafana
JMeter webinar - integration with InfluxDB and GrafanaJMeter webinar - integration with InfluxDB and Grafana
JMeter webinar - integration with InfluxDB and Grafana
 
Introduction to Open Source RAG and RAG Evaluation
Introduction to Open Source RAG and RAG EvaluationIntroduction to Open Source RAG and RAG Evaluation
Introduction to Open Source RAG and RAG Evaluation
 
Kubernetes & AI - Beauty and the Beast !?! @KCD Istanbul 2024
Kubernetes & AI - Beauty and the Beast !?! @KCD Istanbul 2024Kubernetes & AI - Beauty and the Beast !?! @KCD Istanbul 2024
Kubernetes & AI - Beauty and the Beast !?! @KCD Istanbul 2024
 
Structuring Teams and Portfolios for Success
Structuring Teams and Portfolios for SuccessStructuring Teams and Portfolios for Success
Structuring Teams and Portfolios for Success
 
UiPath Test Automation using UiPath Test Suite series, part 1
UiPath Test Automation using UiPath Test Suite series, part 1UiPath Test Automation using UiPath Test Suite series, part 1
UiPath Test Automation using UiPath Test Suite series, part 1
 
In-Depth Performance Testing Guide for IT Professionals
In-Depth Performance Testing Guide for IT ProfessionalsIn-Depth Performance Testing Guide for IT Professionals
In-Depth Performance Testing Guide for IT Professionals
 
Agentic RAG What it is its types applications and implementation.pdf
Agentic RAG What it is its types applications and implementation.pdfAgentic RAG What it is its types applications and implementation.pdf
Agentic RAG What it is its types applications and implementation.pdf
 
ECS 2024 Teams Premium - Pretty Secure
ECS 2024   Teams Premium - Pretty SecureECS 2024   Teams Premium - Pretty Secure
ECS 2024 Teams Premium - Pretty Secure
 
Free and Effective: Making Flows Publicly Accessible, Yumi Ibrahimzade
Free and Effective: Making Flows Publicly Accessible, Yumi IbrahimzadeFree and Effective: Making Flows Publicly Accessible, Yumi Ibrahimzade
Free and Effective: Making Flows Publicly Accessible, Yumi Ibrahimzade
 

Don't Make Android Bad... Again

  • 3.
  • 4.
  • 5.
  • 6.
  • 7. Don’t Make Android Bad… Again
  • 8.
  • 9. Pedro Vicente Mobile Software Craftsman Apps since ~2009 Self-organising company … Most beautiful person…ever HIRING
  • 10.
  • 11.
  • 12.
  • 14. AsyncTask ruled the (app) world! • Callback hell • Memory leaks • Spaghetti code
  • 16. 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..
  • 17. 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
  • 18. 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?
  • 19. 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…
  • 20. 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
  • 24. Architecture? We have Activities, AsyncTasks and Services… what else do we need?
  • 25.
  • 26.
  • 27. M V P
  • 28. V P M user events updates model updates view state change event
  • 29. V P M
  • 30. 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); }
  • 31. 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); }
  • 32. 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); }
  • 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 with keynote by andrew haskin
  • 35.
  • 36. There are still dev pains to be addressed.
  • 37.
  • 38. 1:1 dependency Lifecycle? State on rotation? State on rotation? Testability?
  • 39. 1:1 dependency Lifecycle? State on rotation? Testability?
  • 40. 1:1 dependency Lifecycle? Do we want to? State on rotation? Testability?
  • 42. 1:N
  • 43. 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 } }
  • 44. 1:N Yes you’re right, this does not solve them all
  • 45.
  • 47. Some assumptions… It’s 2018. You don’t live under a rock You use (at least some of):
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 55. Behavior Download Manager Media & Playback Permissions Notifications Sharing Slices
  • 58. UI
  • 59. UI Animation & Transisiton
 Auto, TV & Wear Emoji Fragment Layout Palette
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 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 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
  • 72. 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
  • 73. 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
  • 74. 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) }
  • 75. 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 💥
  • 76. 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) }
  • 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) }
  • 78.
  • 79. <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) }
  • 80.
  • 81.
  • 82. With this you easily get a lot of magic UI!
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90. 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) }
  • 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!
  • 98. Any questions? made with keynote by andrew haskin
  • 99. Join us at
 Android Portugal Slack http://bit.ly/AndroidPortugal made with keynote by andrew haskin
  • 100. made with keynote Thanks to Andrew Haskin Carlos Mota & Renato Almeida Kamil Seweryninspiring article review and opinions base slides