Tìm hiểu lập trình Android bằng cách làm ứng dụng tìm kiếm truyện

Trong bài viết này chúng ta sẽ tạo ra một ứng dụng tìm kiếm sách từ nguồn Open library API và kết quả tìm được sẽ là danh sách quyển sách kèm theo hình ảnh. Đồng thời ta thêm phần giới thiệu, chia sẻ cho bạn bè về sách mà người dùng tâm đắc.

Tổng quan về ứng dụng

Ứng dụng này sẽ có 2 màn hình, một màn hình là danh sách các sách bao gồm các thông tin là tiêu đề, tác giả và một bức ảnh. Và khi người dùng chạm vào một sách thì sẽ mở ra chi tiết của sách đó bao gồm thông tin xuất bản, số trang.

list.png

detail.png

Những bài học thu được trong bài hướng dẫn này

  • Tìm hiểu về json, và làm việc với dữ liệu gửi dạng json
  • Cách làm việc với phần tìm kiếm ở phần trên ứng dụng.
  • Cách sử dụng Progress bar
  • Cách truyền và gửi dữ liệu giữa các màn hình

Tiến hành

Khởi tạo ứng dụng

Tạo mới một project trong Android Studio và đặt tên là "Book search". Chọn class mặc định là 'BookListActivity' Thêm 2 thư viện cho phần làm việc kết nối mạng cũng như việc load ảnh là 2 thư viên android-async-httpPicasso. 2 thư viện này được khai báo ở file build.gradle

dependencies {
  // ...
  compile 'com.loopj.android:android-async-http:1.4.9'
  compile 'com.squareup.picasso:picasso:2.5.2'
}

Làm giao diện trang danh sách truyện

Giao diện danh sách truyện được làm ở file res/layout/activity_book_list.xml với nội dung như sau

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  tools:context=".BookListActivity">

  <ListView
    android:id="@+id/lvBooks"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
</LinearLayout>

Và nếu chạy thử thì sẽ hiển thị như sau item.png

Kết nối đến API

Ta sẽ tạo một class để làm việc với API về truyện và đặt tên class này là BookClient với nội dung như sau

public class BookClient {
  private static final String API_BASE_URL = "http://openlibrary.org/";
  private AsyncHttpClient client;

  public BookClient() {
    this.client = new AsyncHttpClient();
  }

  private String getApiUrl(String relativeUrl) {
    return API_BASE_URL + relativeUrl;
  }

  // Method for accessing the search API
  public void getBooks(final String query, JsonHttpResponseHandler handler) {
    try {
      String url = getApiUrl("search.json?q=");
      client.get(url + URLEncoder.encode(query, "utf-8"), handler);
    } catch (UnsupportedEncodingException e) {
      e.printStackTrace();
    }
  }
}

Chúng ta sẽ xem qua dữ liệu API trả về sẽ như thế nào Sau khi gửi yêu cầu lên server thì chúng ta sẽ nhận được dữ liệu về sách dưới dạng như sau

[
  {
    "title_suggest":"Lord of the Rings",
    "edition_key":[
      "OL9409698M",
      "OL10957365M",
      "OL10957364M",
      "OL9314653M",
      "OL9701406M"
    ],
    "cover_i":1454705,
    "isbn":[
      "0768325293",
      "9780768374698",
      "9780768325782",
      "9780768325249",
      "9780768325294",
      "0768325242",
      "0768325234",
      "0768374693",
      "0768325781",
      "9780768325232"
    ],
    "has_fulltext":false,
    "text":[
      "OL9409698M",
      "OL10957365M",
      "OL10957364M",
      "OL9314653M",
      "OL9701406M",
      "0768325293",
      "9780768374698",
      "9780768325782",
      "9780768325249",
      "9780768325294",
      "0768325242",
      "0768325234",
      "0768374693",
      "0768325781",
      "9780768325232",
      "Cedco Publishing",
      "OL2853379A",
      "Journal # 4 (Lord of the Rings)",
      "Trilogy Edition 2006 Calendar (Cedco Mini Daily)",
      "Journal # 3 (Lord of the Rings)",
      "Lord of the Rings",
      "\/works\/OL8527426W",
      "Cedco Publishing Company",
      "The Lord of the Rings"
    ],
    "author_name":[
    "Cedco Publishing"
    ],
    "seed":[
    "\/books\/OL9409698M",
    "\/books\/OL10957365M",
    "\/books\/OL10957364M",
    "\/books\/OL9314653M",
    "\/books\/OL9701406M",
    "\/works\/OL8527426W",
    "\/authors\/OL2853379A"
    ],
    "author_key":[
    "OL2853379A"
    ],
    "title":"Lord of the Rings",
    "publish_date":[
    "August 30, 2005",
    "September 2002",
    "October 2001"
    ],
    "type":"work",
    "ebook_count_i":0,
    "edition_count":5,
    "key":"\/works\/OL8527426W",
    "id_goodreads":[
    "130022",
    "5984308",
    "59318"
    ],
    "publisher":[
    "Cedco Publishing Company"
    ],
    "language":[
    "eng"
    ],
    "last_modified_i":1282021198,
    "id_librarything":[
    "4607237"
    ],
    "cover_edition_key":"OL9701406M",
    "publish_year":[
    2002,
    2001,
    2005
    ],
    "first_publish_year":2001
    },
    {
      "title_suggest":"Ring out, wild bells",
      ...
    }
  ]

Khai báo Model Book

Dựa vào dữ liệu json trả về ta xây dựng lớp định nghĩa về đối tượng truyện như sau

public class Book {
  private String openLibraryId;
  private String author;
  private String title;

  public String getOpenLibraryId() {
    return openLibraryId;
  }

  public String getTitle() {
    return title;
  }

  public String getAuthor() {
    return author;
  }

  // Get medium sized book cover from covers API
  public String getCoverUrl() {
    return "http://covers.openlibrary.org/b/olid/" + openLibraryId + "-M.jpg?default=false";
  }

  // Get large sized book cover from covers API
  public String getLargeCoverUrl() {
    return "http://covers.openlibrary.org/b/olid/" + openLibraryId + "-L.jpg?default=false";
  }
}

Tiếp đến ta làm 1 lớp để xử lý dữ liệu từ json trả về và gán nó thành đối tượng Book vừa làm ở trên. Nội dung source như sau

public class Book {
  ...

  // Returns a Book given the expected JSON
  public static Book fromJson(JSONObject jsonObject) {
    Book book = new Book();
    try {
      // Deserialize json into object fields
      // Check if a cover edition is available
      if (jsonObject.has("cover_edition_key"))  {
        book.openLibraryId = jsonObject.getString("cover_edition_key");
      } else if(jsonObject.has("edition_key")) {
        final JSONArray ids = jsonObject.getJSONArray("edition_key");
        book.openLibraryId = ids.getString(0);
      }
      book.title = jsonObject.has("title_suggest") ? jsonObject.getString("title_suggest") : "";
      book.author = getAuthor(jsonObject);
    } catch (JSONException e) {
      e.printStackTrace();
      return null;
    }
    // Return new object
    return book;
  }

  // Return comma separated author list when there is more than one author
  private static String getAuthor(final JSONObject jsonObject) {
    try {
      final JSONArray authors = jsonObject.getJSONArray("author_name");
      int numAuthors = authors.length();
      final String[] authorStrings = new String[numAuthors];
      for (int i = 0; i < numAuthors; ++i) {
        authorStrings[i] = authors.getString(i);
      }
      return TextUtils.join(", ", authorStrings);
    } catch (JSONException e) {
      return "";
    }
  }
   public static ArrayList<Book> fromJson(JSONArray jsonArray) {
    ArrayList<Book> books = new ArrayList<Book>(jsonArray.length());
    // Process each result in json array, decode and convert to business
    // object
    for (int i = 0; i < jsonArray.length(); i++) {
      JSONObject bookJson = null;
      try {
        bookJson = jsonArray.getJSONObject(i);
      } catch (Exception e) {
        e.printStackTrace();
        continue;
      }
      Book book = Book.fromJson(bookJson);
      if (book != null) {
        books.add(book);
      }
    }
    return books;
  }

}

Để có thể đưa vào danh sách các truyện thì ta cần tạo 1 adapter sẽ là các mảng truyện được đưa vào. Để làm được thì cần khai báo class như sau

public class BookAdapter extends ArrayAdapter<Book> {

  public BookAdapter(Context context, ArrayList<Book> aBooks) {
    super(context, 0, aBooks);
  }

  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
    // TODO: Complete the definition of the view for each book
    return null;
  }
}

Dựng bố cục cho từng truyện trong danh sách

Bây giờ ta cần dựng bố cục từng truyện trong danh sách. Ở đây ta sẽ để ảnh bên trái và bên phải dòng trên là tiêu đề truyện, dòng dưới là tác giả. Chúng ta sẽ tạo 1 file là item_book.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="vertical">

  <ImageView
    android:id="@+id/ivBookCover"
    android:layout_width="56dp"
    android:layout_height="56dp"
    android:layout_margin="8dp" />

  <TextView
    android:id="@+id/tvTitle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    android:layout_toRightOf="@+id/ivBookCover"
    android:text="Title"
    android:textSize="24dp" />

  <TextView
    android:id="@+id/tvAuthor"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="8dp"
    android:layout_toRightOf="@+id/ivBookCover"
    android:layout_below="@+id/tvTitle"
    android:text="Author"
    android:textSize="12dp" />
</RelativeLayout>

Và ta có thể nạp truyện từ Adapter vào view này như sau

public class BookAdapter extends ArrayAdapter<Book> {
  // View lookup cache
  private static class ViewHolder {
    public ImageView ivCover;
    public TextView tvTitle;
    public TextView tvAuthor;
  }

  public BookAdapter(Context context, ArrayList<Book> aBooks) {
    super(context, 0, aBooks);
  }

  // Translates a particular `Book` given a position
  // into a relevant row within an AdapterView
  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
    // Get the data item for this position
    final Book book = getItem(position);
    // Check if an existing view is being reused, otherwise inflate the view
    ViewHolder viewHolder; // view lookup cache stored in tag
    if (convertView == null) {
      viewHolder = new ViewHolder();
      LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
      convertView = inflater.inflate(R.layout.item_book, parent, false);
      viewHolder.ivCover = (ImageView)convertView.findViewById(R.id.ivBookCover);
      viewHolder.tvTitle = (TextView)convertView.findViewById(R.id.tvTitle);
      viewHolder.tvAuthor = (TextView)convertView.findViewById(R.id.tvAuthor);
      convertView.setTag(viewHolder);
    } else {
      viewHolder = (ViewHolder) convertView.getTag();
    }
    // Populate the data into the template view using the data object
    viewHolder.tvTitle.setText(book.getTitle());
    viewHolder.tvAuthor.setText(book.getAuthor());
    Picasso.with(getContext()).load(Uri.parse(book.getCoverUrl())).error(R.drawable.ic_nocover).into(viewHolder.ivCover);
    // Return the completed view to render on screen
    return convertView;
  }
}

Màn hình danh sách truyện

Sau khi đã có các phần trên giờ ta cần chạy các class truyện và kết nối đến server rồi điền vào view trong BookListActivity tạo từ đầu như sau

public class BookListActivity extends AppCompatActivity {
  private ListView lvBooks;
  private BookAdapter bookAdapter;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_book_list);
    lvBooks = (ListView) findViewById(R.id.lvBooks);
    ArrayList<Book> aBooks = new ArrayList<Book>();
    bookAdapter = new BookAdapter(this, aBooks);
    lvBooks.setAdapter(bookAdapter);
    fetchBooks();
  }
  // Executes an API call to the OpenLibrary search endpoint, parses the results
  // Converts them into an array of book objects and adds them to the adapter
  private void fetchBooks() {
    client = new BookClient();
    client.getBooks("oscar Wilde", new JsonHttpResponseHandler() {
      @Override
      public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
        try {
          JSONArray docs = null;
          if(response != null) {
            // Get the docs json array
            docs = response.getJSONArray("docs");
            // Parse json array into array of model objects
            final ArrayList<Book> books = Book.fromJson(docs);
            // Remove all books from the adapter
            bookAdapter.clear();
            // Load model objects into the adapter
            for (Book book : books) {
              bookAdapter.add(book); // add book through the adapter
            }
            bookAdapter.notifyDataSetChanged();
          }
        } catch (JSONException e) {
          // Invalid JSON format, show appropriate error.
          e.printStackTrace();
        }
      }
    });
  }
}

Sau đó chạy thử app ta sẽ có màn hình danh sách các truyện.

Thêm chức năng tìm kiếm

Ta xây dựng chức năng tìm kiếm truyện theo tiêu đề và tác giả. Để thêm phần tìm kiếm trong Action bar ta tạo file menu_book_list.xml như sau:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  tools:context=".BookListActivity">
  <item
    android:id="@+id/action_search"
    android:title="Search"
    android:icon="@android:drawable/ic_menu_search"
    app:showAsAction="always"
    app:actionViewClass="android.support.v7.widget.SearchView" />
</menu>

Bây giờ ta cần nhận thông tin từ phần tìm kiếm này để cho vào BoookListActivity và sử dụng progress bar

public class BookListActivity extends AppCompatActivity {
  ...
  private ProgressBar progress;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ...
    progress = (ProgressBar) findViewById(R.id.progress);
  }

  // Executes an API call to the OpenLibrary search endpoint, parses the results
  // Converts them into an array of book objects and adds them to the adapter
  private void fetchBooks(String query) {
    // Show progress bar before making network request
    progress.setVisibility(ProgressBar.VISIBLE);
    ...

    client.getBooks(query, new JsonHttpResponseHandler() {
      @Override
      public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
        try {
          // hide progress bar
          progress.setVisibility(ProgressBar.GONE);
          ...
        } catch (JSONException e) {
          // Invalid JSON format, show appropriate error.
          e.printStackTrace();
        }
      }

      @Override
      public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) {
        progress.setVisibility(ProgressBar.GONE);
      }
    });
  }
}

Màn hình chi tiết truyện

Đầu tiên ta tạo 1 activity có tên là BookDetailActivity và đồng thời tạo layout là activity_book_detail.xml sẽ hiển thị thông tin sách, nhà xuất bản và ảnh

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  tools:context=".BookListActivity">

  <ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
      android:gravity="center_horizontal"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:orientation="vertical">

      <ImageView
        android:id="@+id/ivBookCover"
        android:layout_marginTop="8dp"
        android:layout_width="280dp"
        android:layout_height="280dp" />

      <TextView
        android:id="@+id/tvTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="32sp"
        android:layout_marginTop="8dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp" />

      <TextView
        android:id="@+id/tvAuthor"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="22sp"
        android:layout_marginTop="8dp" />

      <View
        android:background="@android:color/darker_gray"
        android:layout_width="280dp"
        android:layout_height="1px"
        android:layout_marginTop="8dp" />

      <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="8dp"
      android:text="Published By"
      android:textSize="14sp" />

      <TextView
      android:id="@+id/tvPublisher"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textSize="14sp" />

      <TextView
      android:id="@+id/tvPageCount"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="8dp"
      android:textSize="14sp" />
    </LinearLayout>
  </ScrollView>
</LinearLayout>

Và bây giờ tao tạo sự kiện khi chạm vào từng truyện sẽ chuyển sang trang chi tiết

public class BookListActivity extends AppCompatActivity {
  public static final String BOOK_DETAIL_KEY = "book";
  ...

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ...
    setupBookSelectedListener();
  }

  public void setupBookSelectedListener() {
    lvBooks.setOnItemClickListener(new AdapterView.OnItemClickListener() {
      @Override
      public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        // Launch the detail view passing book as an extra
        Intent intent = new Intent(BookListActivity.this, BookDetailActivity.class);
        intent.putExtra(BOOK_DETAIL_KEY, bookAdapter.getItem(position));
        startActivity(intent);
      }
    });
  }

  ...
}

Vì trang chi tiết truyện cần thông tin khác nữa vì thế ta cần thêm một hàm lấy thông tin chi tiết của 1 truyện gọi từ server

public class BookClient {
  ...

  // Method for accessing books API to get publisher and no. of pages in a book.
  public void getExtraBookDetails(String openLibraryId, JsonHttpResponseHandler handler) {
    String url = getApiUrl("books/");
    client.get(url + openLibraryId + ".json", handler);
  }
}

Bây giờ ta sẽ lấy thông tin truyện và truyền vào view vừa tạo ở trên

public class BookDetailActivity extends AppCompatActivity {
  private ImageView ivBookCover;
  private TextView tvTitle;
  private TextView tvAuthor;
  private TextView tvPublisher;
  private TextView tvPageCount;
  private BookClient client;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_book_detail);
    // Fetch views
    ivBookCover = (ImageView) findViewById(R.id.ivBookCover);
    tvTitle = (TextView) findViewById(R.id.tvTitle);
    tvAuthor = (TextView) findViewById(R.id.tvAuthor);
    tvPublisher = (TextView) findViewById(R.id.tvPublisher);
    tvPageCount = (TextView) findViewById(R.id.tvPageCount);
    // Use the book to populate the data into our views
    Book book = (Book) getIntent().getSerializableExtra(BookListActivity.BOOK_DETAIL_KEY);
    loadBook(book);
  }

  // Populate data for the book
  private void loadBook(Book book) {
    //change activity title
    this.setTitle(book.getTitle());
    // Populate data
    Picasso.with(this).load(Uri.parse(book.getLargeCoverUrl())).error(R.drawable.ic_nocover).into(ivBookCover);
    tvTitle.setText(book.getTitle());
    tvAuthor.setText(book.getAuthor());
    // fetch extra book data from books API
    client = new BookClient();
    client.getExtraBookDetails(book.getOpenLibraryId(), new JsonHttpResponseHandler() {
      @Override
      public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
        try {
          if (response.has("publishers")) {
            // display comma separated list of publishers
            final JSONArray publisher = response.getJSONArray("publishers");
            final int numPublishers = publisher.length();
            final String[] publishers = new String[numPublishers];
            for (int i = 0; i < numPublishers; ++i) {
              publishers[i] = publisher.getString(i);
            }
            tvPublisher.setText(TextUtils.join(", ", publishers));
          }
          if (response.has("number_of_pages")) {
            tvPageCount.setText(Integer.toString(response.getInt("number_of_pages")) + " pages");
          }
        } catch (JSONException e) {
          e.printStackTrace();
        }
      }
      });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
      // Inflate the menu; this adds items to the action bar if it is present.
      getMenuInflater().inflate(R.menu.menu_book_detail, menu);
      return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
      // Handle action bar item clicks here. The action bar will
      // automatically handle clicks on the Home/Up button, so long
      // as you specify a parent activity in AndroidManifest.xml.
      int id = item.getItemId();

      //noinspection SimplifiableIfStatement
      if (id == R.id.action_settings) {
        return true;
      }

      return super.onOptionsItemSelected(item);
    }
}

Thêm phần chia sẻ ảnh

Để thêm phần chia sẻ ảnh đầu tiên ta cần xin thêm 2 quyền để lấy ảnh trong truyện và ta xin quyền này trong file AndroidManifest.xml

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

Và giờ tạo view cho phần chia sẻ

<menu xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  tools:context=".BookListActivity">
  <item
    android:id="@+id/action_share"
    app:showAsAction="ifRoom"
    android:icon="@android:drawable/ic_menu_share"
    android:title="Share" />
</menu>

Và bắt sự kiện khi click vào chia sẻ

public class BookDetailActivity extends AppCompatActivity {
  ...

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();

    if (id == R.id.action_share) {
      setShareIntent();
      return true;
    }
    return super.onOptionsItemSelected(item);
  }

  private void setShareIntent() {
    ImageView ivImage = (ImageView) findViewById(R.id.ivBookCover);
    final TextView tvTitle = (TextView)findViewById(R.id.tvTitle);
    // Get access to the URI for the bitmap
    Uri bmpUri = getLocalBitmapUri(ivImage);
    // Construct a ShareIntent with link to image
    Intent shareIntent = new Intent();
    // Construct a ShareIntent with link to image
    shareIntent.setAction(Intent.ACTION_SEND);
    shareIntent.setType("*/*");
    shareIntent.putExtra(Intent.EXTRA_TEXT, (String)tvTitle.getText());
    shareIntent.putExtra(Intent.EXTRA_STREAM, bmpUri);
    // Launch share menu
    startActivity(Intent.createChooser(shareIntent, "Share Image"));
  }

  // Returns the URI path to the Bitmap displayed in cover imageview
  public Uri getLocalBitmapUri(ImageView imageView) {
    // Extract Bitmap from ImageView drawable
    Drawable drawable = imageView.getDrawable();
    Bitmap bmp = null;
    if (drawable instanceof BitmapDrawable){
      bmp = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
    } else {
      return null;
    }
    // Store image to default external storage directory
    Uri bmpUri = null;
    try {
      File file =  new File(Environment.getExternalStoragePublicDirectory(
      Environment.DIRECTORY_DOWNLOADS), "share_image_" + System.currentTimeMillis() + ".png");
      file.getParentFile().mkdirs();
      FileOutputStream out = new FileOutputStream(file);
      bmp.compress(Bitmap.CompressFormat.PNG, 90, out);
      out.close();
      bmpUri = Uri.fromFile(file);
    } catch (IOException e) {
      e.printStackTrace();
    }
    return bmpUri;
  }
}

Tổng kết

Như vậy sau khi thực hiện các bước trên ta tạo được một ứng dụng tìm truyện và xem chi tiết truyện. Đồng thời cũng hiểu được cách sử dụng kết nối để server API để lấy thông tin. Thêm vào nữa cách truyền dữ liệu từ màn hình này sang màn hình kia trong lập trình Android. Nguồn: http://guides.codepath.com/android/Book-Search-Tutorial


All Rights Reserved