Mosby翻译(二) : 入门

英文原文

这里有一些如何使用Mosby来建立一个基于MVP的架构的简单的例子。我们假设你已经知道“MVP - passive view”,如果不是,你应该考虑阅读 Mosby翻译(三) : MVP原理,以便在继续之前了解MVP的基本知识。

Hello MVP World

在这个非常简单的例子中,我们假设view要显示一个TextView,这个TextView显示“Hello”和一个随机数字,或者“Goodbye”和一个随机数字。如果显示“Hello”,则文字颜色将变为红色,如果是“Goodbye”则将以蓝色文字显示。生成问候文本是在一个AsyncTask里完成的,我们假设生成问候文本(随机数计算并连接在问候文本的结尾)是耗CPU的,并且需要2秒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// "Business logic" component
class GreetingGeneratorTask extends AsyncTask<Void, Void, Integer> {

// Callback - Listener
public interface GreetingTaskListener{
public void onGreetingGenerated(String greetingText);
}

private String baseText;
private GreetingTaskListener listener;

public GreetingGeneratorTask(String baseText, GreetingTaskListener listener){
this.baseText = baseText;
this.listener = listener;
}

// Simulates computing and returns a random integer
@Override
protected Integer doInBackground(Void... params) {
try {
Thread.sleep(2000); // Simulate computing
} catch (InterruptedException e) { }

return (int) (Math.random() * 100);
}

@Override
protected void onPostExecute(Integer randomInt){
listener.onGreetingGenerated(baseText + " "+randomInt);
}
}
1
2
3
4
5
6
7
8
9
// View interface
interface HelloWorldView extends MvpView {

// displays "Hello" greeting text in red text color
void showHello(String greetingText);

// displays "Goodbye" greeting text in blue text color
void showGoodbye(String greetingText);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// The presenter that coordinates HelloWorldView and business logic (GreetingGeneratorTask)
class HelloWorldPresenter extends MvpBasePresenter<HelloWorldView> {

// Greeting Task is "business logic"
private GreetingGeneratorTask greetingTask;

private void cancelGreetingTaskIfRunning(){
if (greetingTask != null){
greetingTask.cancel(true);
}
}

public void greetHello(){
cancelGreetingTaskIfRunning();

greetingTask = new GreetingGeneratorTask("Hello", new GreetingTaskListener(){
public void onGreetingGenerated(String greetingText){
if (isViewAttached())
getView().showHello(greetingText);
}
});
greetingTask.execute();
}

public void greetGoodbye(){
cancelGreetingTaskIfRunning();

greetingTask = new GreetingGeneratorTask("Goodbye", new GreetingTaskListener(){
public void onGreetingGenerated(String greetingText){
if (isViewAttached())
getView().showGoodbye(greetingText);
}
});
greetingTask.execute();
}
1
2
3
4
5
6
7
8
  // Called when Activity gets destroyed, so cancel running background task
public void detachView(boolean retainPresenterInstance){
super.detachView(retainPresenterInstance);
if (!retainPresenterInstance){
cancelGreetingTaskIfRunning();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class HelloWorldActivity extends MvpActivity<HelloWorldView, HelloWorldPresenter>
implements HelloWorldView {

@Bind(R.id.greetingTextView) TextView greetingTextView;

@Override
protected void onCreate(Bundle savedState){
super.onCreate(savedState);
setContentView(R.layout.activity_helloworld);
Butterknife.bind(this);
}

@Override // Called internally by Mosby
public HelloWorldPresenter createPresenter(){
return new HelloWorldPresenter();
}

@OnClick(R.id.helloButton)
public void onHelloButtonClicked(){
presenter.greetHello();
}

@OnClick(R.id.goodbyeButtonClicked)
public void onGoodbyeButtonClicked(){
presenter.greetGoodbye();
}

@Override
public void showHello(String greetingText){
greetingTextView.setTextColor(Color.RED);
greetingTextView.setText(greetingText);
}

@Override
public void showGoodbye(String greetingText){
greetingTextView.setTextColor(Color.BLUE);
greetingTextView.setText(greetingText);
}
}

正如你在HelloWorldActivity中所看到的,只包含与UI组件相关的代码。没有业务逻辑,没有1000多行乱七八糟代码的Activity。通过View(HelloWorldActivity),Presenter(HelloWorldPresenter)和业务逻辑(GreetingGeneratorTask)之间的明确分离,您可以编写可维护,松耦合和可测试的代码。这就是MVP的全部内容

LCE例子

下面这个例子稍微复杂一些,更接近真实世界的应用程序。假设我们想要在RecyclerView中显示一个国家列表。国家列表从Web服务(异步)加载,并且需要几秒钟。我们将使用Retrofit作为http库。加载时我们要显示一个ProgressBar。如果发生错误,我们希望显示错误消息。此外,我们要使用一个SwipeRefreshLayout,使用户可以刷新国家列表。我们把这类View叫做LCE-View(Loading-Content-Error),因为View有三种状态:显示加载,显示内容和显示错误视图。Mosby为这样的Views提供了一个模板:MvpLceActivityMvpLceFragment。这些模板的基类中会处理更改视图状态的操作(使用淡入淡出的动画)。

1
2
3
4
5
6
// Business logic: We use Retrofit to load a list of Countries from a web service
interface CountriesApi {

@GET("/countries")
void getCountries(Callback<List<Country>> callback);
}
1
2
3
4
5
6
7
8
9
// View interface
interface CountriesView extends MvpLceView<List<Country>> {
// MvpLceView already defines LCE methods:
//
// void showLoading(boolean pullToRefresh)
// void showError(Throwable t, boolean pullToRefresh)
// void setData(List<Country> data)
// void showContent()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class CountriesPresenter extends MvpBasePresenter<CountriesView> {

private CountriesApi api;

public void loadCountries(final boolean pullToRefresh){

api.getCountries(new Callback<List<Country>>(){
@Override
public void success(List<Country> countries, Response response) {
if (isViewAttached()){
getView().setData(countries);
getView().showContent();
}
}

@Override
public void failure(RetrofitError retrofitError) {
if(isViewAttached())
getView().showError(retrofitError.getCause(), pullToRefresh);
}
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class CountriesFragment
extends MvpLceFragment<SwipeRefreshLayout, List<Country>, CountriesView, CountriesPresenter>
implements CountriesView, SwipeRefreshLayout.OnRefreshListener {

@Bind(R.id.recyclerView) RecyclerView recyclerView;
CountriesAdapter adapter;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_countries, container, false);
}

@Override public void onViewCreated(View view, Bundle savedInstance) {
super.onViewCreated(view, savedInstance);
ButterKnife.bind(this, view);
// contentView is SwipeRefreshLayout
contentView.setOnRefreshListener(this);

// Setup recycler view
adapter = new CountriesAdapter(getActivity());
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setAdapter(adapter);
loadData(false);
}

@Override public void loadData(boolean pullToRefresh) {
presenter.loadCountries(pullToRefresh);
}

@Override public void onRefresh() {
loadData(true);
}

@Override public CountriesPresenter createPresenter() {
return new CountriesPresenter();
}

@Override public void setData(List<Country> data) {
adapter.setCountries(data);
adapter.notifyDataSetChanged();
}

@Override public void showContent() {
super.showContent();
contentView.setRefreshing(false);
}

@Override public void showError(Throwable e, boolean pullToRefresh) {
super.showError(e, pullToRefresh);
contentView.setRefreshing(false);
}

@Override public void showLoading(boolean pullToRefresh) {
super.showLoading(pullToRefresh);
contentView.setRefreshing(pullToRefresh);
}
}

LCE ViewState示例

在之前的LCE示例中如果用户旋转屏幕,会发生什么?一个新的Fragment被创建,再次显示ProgressBar,即使在屏幕方向改变之前,我们已经显示了数据(国家列表)。Mosby提供了一个称为ViewState的功能来处理屏幕方向的变化。查看ViewState部分来查看更多和其工作原理。在这个“入门”部分,我们只是想展示如何将ViewState支持添加到之前的LCE示例,以便您的应用程序仍然处于屏幕方向更改之前的状态,即在竖屏时显示国家列表,在横屏时仍然显示国家列表。我们所要做的就是继承MvpLceViewStateFragment而不是MvpLceFragment,并且实现createViewState()getData()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class CountriesFragment
extends MvpLceViewStateFragment<SwipeRefreshLayout, List<Country>, CountriesView, CountriesPresenter>
implements CountriesView, SwipeRefreshLayout.OnRefreshListener {

@Bind(R.id.recyclerView) RecyclerView recyclerView;
CountriesAdapter adapter;

@Override
public void onCreate(Bundle savedState){
super.onCreate(savedState);
setRetainInstance(true); // Enable retaining presenter / viewstate
}

@Override public LceViewState<List<Country>, CountriesView> createViewState() {
return new RetainingLceViewState<List<Country>, CountriesView>();
}

@Override public List<Country> getData() {
return adapter == null ? null : adapter.getCountries();
}

...
// Everything else remains the same as shown in the previous LCE example

}

请注意,Activities和ViewGroup也支持ViewState功能。

Kotlin示例

Kotlin是Android开发中的新希望。从Mosby 2.0开始支持Kotlin。在这个例子中,我们将展示一个非常基本的LCE示例,在这个例子中,我们生成(异步)超级英雄列表,并将其显示在RecyclerView中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Business logic: AsyncTask generates list of heroes and invokes successful lambda or error lambda as "callback"
public class AsyncHeroesTask(val pullToRefresh: Boolean,
val successful: (List<Hero>, Boolean) -> Unit,
val error: (Exception, Boolean) -> Unit) : AsyncTask<Void, Void, List<Hero>>() {

// Simple static counter since we simulate errors on every second request
companion object Counter {
var requestCounter: Int = 0
}


override fun doInBackground(vararg params: Void?): List<Hero>? {

Thread.sleep(2000) // Simulate network delay

requestCounter++

// Simulate network error every second request --> returning null means error
if (requestCounter % 2 != 0) return null;


var heroes = arrayListOf(
Hero("Batwoman", "https://upload.wikimedia.org/wikipedia/en/2/24/Batwoman.png"),
Hero("Spider-Man","http://oyster.ignimgs.com/wordpress/stg.ign.com/2014/09/1j-720x1091.jpg"),
...
)

Collections.shuffle(heroes)

return heroes;
}

override fun onPostExecute(heroes: List<Hero>?) {
when (heroes) {
null -> error(Exception(), pullToRefresh)
else -> successful(heroes, pullToRefresh)
}
}
}
1
2
3
interface HeroesView : MvpLceView<List<Hero>> {
// LCE methods inherited
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class HeroesPresenter : MvpBasePresenter<HeroesView> () {

private var loaderTask: AsyncHeroesTask ? = null

fun loadHeroes(pullToRefresh: Boolean) {

cancelIfRunning();

// Show Loading
view?.showLoading(pullToRefresh)

// execute loading
loaderTask = AsyncHeroesTask(
pullToRefresh,
{ heroes, pullToRefresh -> // successful lambda / callback
view?.setData(heroes) // no isViewAttached() check needed because kotlin offers null safety as language feature
view?.showContent()
},
{ exception, pullToRefresh -> // error lambda / callback
view?.showError(exception, pullToRefresh)
}
)

loaderTask?.execute()

}

fun cancelIfRunning() {

// Cancel any previous one
loaderTask?.cancel(true);
}

override fun detachView(retainInstance: Boolean) {
super.detachView(retainInstance)

// Keep async tasks running if retaining, otherwise cancel
if (!retainInstance) {
cancelIfRunning()
}
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class HeroesActivity : HeroesView, MvpLceViewStateActivity<SwipeRefreshLayout, List<Hero>, HeroesView, HeroesPresenter>(), SwipeRefreshLayout.OnRefreshListener {

var adapter: HeroesAdapter? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_heroes)
retainInstance = true
contentView.setOnRefreshListener(this)

val recyclerView = findViewById(R.id.recyclerView) as RecyclerView

adapter = HeroesAdapter(this, LayoutInflater.from(this))
recyclerView.adapter = adapter
recyclerView.layoutManager = GridLayoutManager(this, 2)
}

override fun createPresenter(): HeroesPresenter {
return HeroesPresenter()
}

override fun createViewState(): LceViewState<List<Hero>, HeroesView> {
return RetainingLceViewState()
}

override fun setData(data: List<Hero>?) {
adapter?.items = data
adapter?.notifyDataSetChanged()
}

override fun loadData(pullToRefresh: Boolean) {
presenter.loadHeroes(pullToRefresh)
}

override fun onRefresh() {
loadData(true)
}

override fun showContent() {
super.showContent()
contentView.isRefreshing = false
}

override fun showError(t: Throwable, pullToRefresh: Boolean) {
super.showError(t, pullToRefresh)
contentView.isRefreshing = false
}

override fun showLoading(pullToRefresh: Boolean) {
super.showLoading(pullToRefresh)
contentView.isRefreshing = pullToRefresh
}
}
坚持原创技术分享,您的支持将鼓励我继续创作!