12/08/2018, 15:47

Tạo Một Ứng Dụng Android Theo Mô Hình Flux Architecture

Trong bài viết này tôi sẽ đi vào thực hành là tạo một ứng dụng nho nhỏ, chứ không đi sâu vào lý thuyết . Nhưng trước tiên tôi sẽ nói qua một chút về flux là gì. Giới thiệu sơ qua về kiến trúc Flux(Flux Architecture) Flux Architecture đã được xây dựng và sử dụng bới Facebook. Mục đích ban đầu ...

Trong bài viết này tôi sẽ đi vào thực hành là tạo một ứng dụng nho nhỏ, chứ không đi sâu vào lý thuyết . Nhưng trước tiên tôi sẽ nói qua một chút về flux là gì.

Giới thiệu sơ qua về kiến trúc Flux(Flux Architecture)

Flux Architecture đã được xây dựng và sử dụng bới Facebook. Mục đích ban đầu của họ khi xây dựng Flux Architecture là cho các dứng dụng web client-side và tất nhiên nó không có ý định xây dựng cho các mobile app.Nhưng với những tính năng và sự đơn giản của nó đã được chuyển đổi rất tốt vào trong nhưng Android Project. Bạn có thể xem chi tiết về Flux tại buổi giới thiệu của Facebook tại đay.

flux-graph-simple.png

Có 2 tính năng chính để bạn có thể hiều về Flux:

  • Luồng dữ liệu luôn luôn là 1 chiều Luồng dữ liệu một chiều là tính năng chính trong Flux Architecture làm cho nó trở lên dễ dàng để học.

  • Ứng dụng là được chia vào 3 phần Chính:

    -View: Giao diện ứng dụng. Nó tạo ra các Action để phản hồi với tương tác của người dùng.

    -Dispatcher: Là một Hub trung tâm, chịu trách nhiệm gửi toàn bộ các Action tới mỗi Store.

    -Store: Duy trì trạnng thái cho ứng dựng, phản hồi lại các Action với các state cụ thể, thực thi logic, phát ra Change Event khi việc xử lý đã xong.Những sự kiện này đã được sử dụng bởi View cho việc update giao diện. Cả 3 phần giao tiếp với nhau bằng Actions. Action là đối tượng đơn giản được xác định bởi một Type của Action, và chứa data liên quan tới Action đó.

Flux Android Architecture

flux-graph-complete.png

Trước khi bắt đầu đi sâu vào giải thích code tôi đã làm một video demo để các bạn dễ hình hùng sample của chúng ta như thế nào. Các bạn xem video demo tại đây Ở đây tôi sẽ sử dụng 1 API đã được public , API này sẽ trả về list user dưới định dạng json:

http://api.randomuser.me/?results=30&nat=en

Định dạng data trả về có dạng như sau như sau:

{
    "results": [
        {
            "gender": "male",
            "name": {
                "title": "mr",
                "first": "محمد",
                "last": "جعفری"
            },
            "location": {
                "street": "4658 پاسداران",
                "city": "کرمانشاه",
                "state": "البرز",
                "postcode": 24174
            },
            "email": "محمد.جعفری@example.com",
            "login": {
                "username": "greencat281",
                "password": "1a2b3c",
                "salt": "Z2BZDBif",
                "md5": "4da3307eb0fcd85ddf67ac7e058a5a49",
                "sha1": "46957ac2c6c9c4289b3246b47220df6196facba6",
                "sha256": "7669b6e09d0e0afdd2b580c44132d9eb032d66bd328c070777f203c3352fbd39"
            },
            "dob": "1984-02-02 06:00:35",
            "registered": "2009-09-27 16:23:44",
            "phone": "029-95345035",
            "cell": "0912-733-8663",
            "id": {
                "name": "",
                "value": null
            },
            "picture": {
                "large": "https://randomuser.me/api/portraits/men/76.jpg",
                "medium": "https://randomuser.me/api/portraits/med/men/76.jpg",
                "thumbnail": "https://randomuser.me/api/portraits/thumb/men/76.jpg"
            },
            "nat": "IR"
        },
        {
            "gender": "male",
            "name": {
                "title": "mr",
                "first": "harry",
                "last": "cunningham"
            },
            "location": {
                "street": "9703 railroad st",
                "city": "pembroke pines",
                "state": "nevada",
                "postcode": 39030
            },
            "email": "harry.cunningham@example.com",
            "login": {
                "username": "bluegoose866",
                "password": "1024",
                "salt": "9sSbrNw7",
                "md5": "0644f4818faaa439234bb5389915d3d2",
                "sha1": "212ed1c3bd4f4f3ca95143f6b8bdab560a0bb714",
                "sha256": "1e4da62a3143c4f353c643fcfef4c76c8f99a0b158449b9bef2d26cd7e4242ba"
            },
            "dob": "1970-03-22 08:50:11",
            "registered": "2005-03-18 18:45:05",
            "phone": "(588)-912-0783",
            "cell": "(166)-809-1682",
            "id": {
                "name": "SSN",
                "value": "660-76-3042"
            },
            "picture": {
                "large": "https://randomuser.me/api/portraits/men/8.jpg",
                "medium": "https://randomuser.me/api/portraits/med/men/8.jpg",
                "thumbnail": "https://randomuser.me/api/portraits/thumb/men/8.jpg"
            },
            "nat": "US"
        },
        {
            "gender": "male",
            "name": {
                "title": "mr",
                "first": "george",
                "last": "edwards"
            },
            "location": {
                "street": "2441 fendalton road",
                "city": "invercargill",
                "state": "bay of plenty",
                "postcode": 15757
            },
            "email": "george.edwards@example.com",
            "login": {
                "username": "yellowpanda574",
                "password": "collins",
                "salt": "OoChnUhS",
                "md5": "077abd3d940dc3dd61b8594159a3f662",
                "sha1": "c33d15a8a4cace0610ab472475ab2cdcd5486ccb",
                "sha256": "6d8fe10d0d51a307f60256355a7e6daf6b4a60671ab12e7472c76e58b4877593"
            },
            "dob": "1954-08-06 10:30:50",
            "registered": "2009-04-06 07:28:32",
            "phone": "(189)-448-2642",
            "cell": "(922)-083-2348",
            "id": {
                "name": "",
                "value": null
            },
            "picture": {
                "large": "https://randomuser.me/api/portraits/men/94.jpg",
                "medium": "https://randomuser.me/api/portraits/med/men/94.jpg",
                "thumbnail": "https://randomuser.me/api/portraits/thumb/men/94.jpg"
            },
            "nat": "NZ"
        }
    ],
    "info": {
        "seed": "b4f95d030a7af9da",
        "results": 3,
        "page": 1,
        "version": "1.1"
    }
}

Khi đã biết được cấu trúc data rồi, việc tiếp theo chúng ta cần phải làm là tạo Model cho ứng dụng của chúng ta. Các bạn một package mới với tên là model. Sau đó tạo các Class tương ứng như bên dưới. Để cho gắn gọn, tôi sẽ bỏ đi những method setter và getter. Tạo Class Login

public class Login implements Serializable {

    @SerializedName("username")
    public String mUserName;

    public Login(){}

    public String getUserName() {
        return mUserName;
    }

    public void setUserName(String userName) {
        mUserName = userName;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Login login = (Login) o;

        return mUserName != null ? mUserName.equals(login.mUserName) : login.mUserName == null;

    }

    @Override
    public int hashCode() {
        return mUserName != null ? mUserName.hashCode() : 0;
    }
}

Tạo Class Name

public class Name implements Serializable {

    private static final long serialVersionUID = 1L;

    @SerializedName("title")
    private String mTitle;

    @SerializedName("first")
    private String mFirst;

    @SerializedName("last")
    private String mLast;

    public Name() {
    }
    ........

Tạo Class Location

public class Location implements Serializable {

    private static final long serialVersionUID = 1L;

    @SerializedName("street")
    private String mStreet;

    @SerializedName("city")
    private String mCity;

    @SerializedName("state")
    private String mState;

    @SerializedName("zip")
    private String mZip;

    public Location() {

    }

    public Location(String street, String city, String state, String zip) {
        mStreet = street;
        mCity = city;
        mState = state;
        mZip = zip;
    }
.............

Tạo Class Picture

public class Picture implements Serializable {

    private static final long serialVersionUID = 1L;

    @SerializedName("large")
    private String mLarge;

    @SerializedName("medium")
    private String mMedium;

    @SerializedName("thumbnail")
    private String mThumbnail;

    public Picture() {
    }
    .......

Tạo Class People

public class People implements Serializable {

    private static final long serialVersionUID = 1L;

    @SerializedName("gender")
    private String mGender;

    @SerializedName("name")
    private Name mName;

    @SerializedName("location")
    private Location mLocation;

    @SerializedName("email")
    private String mEmail;

    @SerializedName("login")
    private Login mUserName;

    @SerializedName("cell")
    private String mCell;

    @SerializedName("phone")
    private String mPhone;

    @SerializedName("picture")
    private Picture mPicture;

    private String mFullName;

...........

Chúng ta đã vừa tạo xong phần model. Tiếp theo tôi sẽ tạo các Action dùng để request lên Server lấy dữ liệu về. Khi đã có được data, sẽ gửi những Action trong đó có chứa data đến các store. Các bạn tạo một package với tên là action. Tôi sẽ tạo một Interface. Tạo Interface có tên Actions

public interface Actions {

    String FETCH_PEOPLE = "fetch_people";

    void getPeopleList();

    boolean retry(RxAction action);
}

Các bạn thấy trong Interface tôi vừa tạo có một method tên getPeopleList(). Mothod này dùng để lấy data. Tiếp theo tôi sẽ tạo 1 Class tên PeopleActionCreator implement từ Interface chúng ta vừa tạo ở trên.

public class PeopleActionCreator extends RxActionCreator implements Actions {

    public PeopleActionCreator(Dispatcher dispatcher, SubscriptionManager manager) {
        super(dispatcher, manager);
    }

    @Override
    public void getPeopleList() {
        final String fetch_url = "http://api.randomuser.me/?results=10&nat=en";

        final RxAction action = newRxAction(FETCH_PEOPLE);
        addRxAction(action, PeopleFactory.create().fetchPeople(fetch_url)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(
                        peopleResponse -> {
                            action.getData().put(Keys.PEOPLE, peopleResponse);
                            postRxAction(action);
                        },
                        throwable -> {
                            postRxError(action, throwable);
                        }));
    }

    @Override
    public boolean retry(RxAction action) {
        if (hasRxAction(action)) return true;
        switch (action.getType()) {
            case FETCH_PEOPLE:
                getPeopleList();
                return true;
            default:
                return false;
        }
    }
}

Các bạn chú ý ở đây, trong method getPeopleList(), khi get data success chúng ta sẽ gửi Action với data thới store bằng method postRxAction(action) đã được đinh nghĩa sẵ trong lib rxflux. Nếu có lỗi xảy ra thì gửi lỗi đó postRxError(action, throwable)

Giờ chúng ta sẽ tạo một package có tên là store Tương tự như phần action, tôi cũng tạo một Interface có tên là PeopleStoreInterface

public interface PeopleStoreInterface {

    ArrayList<People> getPeopleList();
}

Tạo một class có tên là PeopleStore implement từ Interface trên

public class PeopleStore extends RxStore implements PeopleStoreInterface {

    public static final String ID = "PeopleList";
    private static PeopleStore sInstance;
    private ArrayList<People> mPeoples;

    public PeopleStore(Dispatcher dispatcher) {
        super(dispatcher);
    }

    public static synchronized PeopleStore getInstance(Dispatcher dispatcher) {
        if (sInstance == null) {
            sInstance = new PeopleStore(dispatcher);
        }
        return sInstance;
    }

    @Override
    public ArrayList<People> getPeopleList() {
        return mPeoples == null ? new ArrayList<>() : mPeoples;
    }

    @Override
    public void onRxAction(RxAction action) {
        switch (action.getType()) {
            case Actions.FETCH_PEOPLE:
                PeopleResponse response = ((PeopleResponse) action.getData().get(Keys.PEOPLE));
                if (response != null) {
                    this.mPeoples = response.getPeopleList();
                }
                break;
            default:
                break;
        }

        postChanged(new RxStoreChange(ID, action));
    }
}

Trong class trên các bạn chú ý đến method onRxAction(RxAction action) phương thức này truyên vào 1 parameter action trong action này có chứ data chúng ta đã lấy về từ serser để có được data PeopleResponse response = ((PeopleResponse) action.getData().get(Keys.PEOPLE)). Sau đó chúng ta sẽ thông báo cho View cũng chính là Activity của chúng ta biết rằng data đã được thay đổi cần được hiển thị bằng method postChanged(new RxStoreChange(ID, action));

Giời đến bước cuối cùng là tạo View hiển thi dữ liệu cho người dùng của chúng ta.

class PeopleActivity extends AppCompatActivity implements RxViewDispatch, PeopleAdapter.OnPeopleClickListener {

    private PeopleAdapter mPeopleAdapter;
    private PeopleStore mPeopleStore;

    private CoordinatorLayout mCoordinator;
    private ProgressBar mProgressBar;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_people);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        mCoordinator = (CoordinatorLayout) findViewById(R.id.coordinator);

        mProgressBar = (ProgressBar) findViewById(R.id.progress_people);
        TextView statusLabel = (TextView) findViewById(R.id.label_status);
        FloatingActionButton fabButton = (FloatingActionButton) findViewById(R.id.fab);
        RecyclerView peopleList = (RecyclerView) findViewById(R.id.list_people);

        peopleList.setHasFixedSize(true);
        mPeopleAdapter = new PeopleAdapter();
        mPeopleAdapter.setCallback(this);

        peopleList.setLayoutManager(new LinearLayoutManager(this));
        peopleList.setAdapter(mPeopleAdapter);

        fabButton.setOnClickListener(view -> {
            showLoadingFrame(true);
            refresh();
        });


    }

    @Override
    protected void onResume() {
        super.onResume();
        refresh();
    }

    @Override
    public void onPeopleClicked(People people) {
        startActivity(PeopleDetailActivity.launchDetail(this, people));
    }

    @Override
    public void onRxStoreChanged(@NonNull RxStoreChange change) {
        showLoadingFrame(false);

        switch (change.getStoreId()) {
            case PeopleStore.ID:
                switch (change.getRxAction().getType()) {
                    case Actions.FETCH_PEOPLE:
                        mPeopleAdapter.setPeopleList(mPeopleStore.getPeopleList());
                        break;
                    default:
                        break;
                }
                break;
            default:
                break;
        }
    }

    @Override
    public void onRxError(@NonNull RxError error) {
        showLoadingFrame(false);
        Throwable throwable = error.getThrowable();
        if (throwable != null) {
            Snackbar.make(mCoordinator, "An error occur :", Snackbar.LENGTH_INDEFINITE)
                    .setAction("Retry", view -> {
                        PeopleApp.get(this).getActionCreator().getPeopleList();
                    });
        } else {
            Toast.makeText(this, "Unknown error", Toast.LENGTH_LONG).show();
        }
    }

    @Override
    public void onRxViewRegistered() {

    }

    @Override
    public void onRxViewUnRegistered() {

    }

    @Nullable
    @Override
    public List<RxStore> getRxStoreListToRegister() {
        mPeopleStore = PeopleStore.getInstance(PeopleApp.get(this).getRxFlux().getDispatcher());
        return Arrays.asList(mPeopleStore);
    }

    @Nullable
    @Override
    public List<RxStore> getRxStoreListToUnRegister() {
        return null;
    }

    private void showLoadingFrame(boolean show) {
        mProgressBar.setVisibility(show ? View.VISIBLE : View.GONE);
    }

    private void refresh() {
        showLoadingFrame(true);
        PeopleApp.get(this).getActionCreator().getPeopleList();
    }
}

Tôi đã implement Activity từ một Interface RxViewDispatch đã được đinh nghĩa trong lib của chúng ta. Các bạn cần lưu ý method onRxStoreChanged, phương thức này nhận về một RxStoreChange có chưa data, từ đó data sẽ được update vào Recyclerview Nếu có lỗi sẽ được hiển thị trong method onRxError Đây là màn hình hiển thị list people

Để hiển thị màn hình People Detail thì rất đơn giản khi đã bắt được sự kiên click trên từng item trong list, ta sẽ có được mỗi people, rồi dùng Intent để gửi data tới một Activity khác. Phần này đơn giản nên tôi không trình bày nữa, các bạn có thể tham khảo chi tiết source code tại đây

0