diff --git a/zero-input.el b/zero-input.el
index 741c5c2ec343bd98b0931cd2622a9f79fac1a810..e4016a5f4d8efec7c259f183a410049284bd60c0 100644
--- a/zero-input.el
+++ b/zero-input.el
@@ -12,7 +12,7 @@
 ;; See the License for the specific language governing permissions and
 ;; limitations under the License.
 
-;; Version: 2.0.6
+;; Version: 2.1.0
 ;; URL: https://gitlab.emacsos.com/sylecn/zero-el
 ;; Package-Requires: ((emacs "24.3") (s "1.2.0"))
 
@@ -243,7 +243,7 @@ If item is not in lst, return nil."
 
 ;; zero-input-el version
 (defvar zero-input-version nil "Zero package version.")
-(setq zero-input-version "2.0.7")
+(setq zero-input-version "2.1.0")
 
 ;; FSM state
 (defconst zero-input--state-im-off 'IM-OFF)
@@ -306,11 +306,14 @@ Change will be effective only in new `zero-input-mode' buffer."
   :group 'zero
   :type 'integer)
 (defvar-local zero-input-current-page 0 "Current page number.  count from 0.")
-(defvar-local zero-input-initial-fetch-size 20
-  "How many candidates to fetch for the first call to GetCandidates.")
+(defvar-local zero-input-initial-fetch-size 21
+  "How many candidates to fetch for the first call to GetCandidates.
+
+It's best set to (1+ (* zero-input-candidates-per-page N)) where
+N is number of pages you want to fetch in initial fetch.")
 ;; zero-input-fetch-size is reset to 0 when preedit-str changes.
-;; zero-input-fetch-size is set to fetch-size in build-candidates-async complete-func
-;; lambda.
+;; zero-input-fetch-size is set to fetch-size in build-candidates-async
+;; complete-func lambda.
 (defvar-local zero-input-fetch-size 0 "Last GetCandidates call's fetch-size.")
 (defvar zero-input-previous-page-key ?\- "Previous page key.")
 (defvar zero-input-next-page-key ?\= "Next page key.")
@@ -419,8 +422,9 @@ enough elements, return lst as it is."
 
 (defun zero-input-show-candidates (&optional candidates)
   "Show CANDIDATES using zero-input-panel via IPC/RPC."
-  (let ((candidates-on-page (zero-input-candidates-on-page (or candidates
-							 zero-input-candidates))))
+  (let ((candidates-on-page
+	 (zero-input-candidates-on-page (or candidates
+					    zero-input-candidates))))
     (cl-destructuring-bind (x y) (zero-input-get-point-position)
       (zero-input-panel-show-candidates
        (funcall zero-input-get-preedit-str-for-panel-func)
@@ -438,19 +442,15 @@ enough elements, return lst as it is."
 (defun zero-input-build-candidates (preedit-str fetch-size)
   "Build candidates list synchronously.
 
-Try to find at least FETCH-SIZE number of candidates for PREEDIT-STR."
+Try to find at least FETCH-SIZE number of candidates for PREEDIT-STR.
+Return a list of candidates."
   ;; (zero-input-debug "zero-input-build-candidates\n")
   (unless (functionp zero-input-build-candidates-func)
     (signal 'wrong-type-argument (list 'functionp zero-input-build-candidates-func)))
-  (prog1 (funcall zero-input-build-candidates-func preedit-str fetch-size)
-    (setq zero-input-fetch-size (max fetch-size (length zero-input-candidates)))))
-
-(defun zero-input-build-candidates-complete (candidates)
-  "Called when `zero-input-build-candidates-async' return.
-
-CANDIDATES is returned candidates list from async call."
-  (setq zero-input-candidates candidates)
-  (zero-input-show-candidates candidates))
+  ;; Note that zero-input-candidates and zero-input-fetch-size is updated in
+  ;; async complete callback. This function only care about building the
+  ;; candidates.
+  (funcall zero-input-build-candidates-func preedit-str fetch-size))
 
 (defun zero-input-build-candidates-async-default (preedit-str fetch-size complete-func)
   "Build candidate list, when done show it via `zero-input-show-candidates'.
@@ -459,6 +459,7 @@ PREEDIT-STR the preedit-str.
 FETCH-SIZE try to find at least this many candidates for preedit-str.
 COMPLETE-FUNC the function to call when build candidates completes."
   ;; (zero-input-debug "zero-input-build-candidates-async-default\n")
+  ;; default implementation just call sync version `zero-input-build-candidates'.
   (let ((candidates (zero-input-build-candidates preedit-str fetch-size)))
     ;; update cache to make SPC and digit key selection possible.
     (funcall complete-func candidates)))
@@ -574,23 +575,27 @@ Return CH's Chinese punctuation if CH is converted.  Return nil otherwise."
   (let ((len (length zero-input-candidates)))
     (when (> len (* zero-input-candidates-per-page (1+ zero-input-current-page)))
       (setq zero-input-current-page (1+ zero-input-current-page))
+      (zero-input-debug "showing candidates on page %s\n" zero-input-current-page)
       (zero-input-show-candidates))))
 
 (defun zero-input-page-down ()
   "If there is still candidates to be displayed, show candidates on next page."
   (interactive)
   (let ((len (length zero-input-candidates))
-	(new-fetch-size (* zero-input-candidates-per-page (+ 2 zero-input-current-page))))
+	(new-fetch-size (1+ (* zero-input-candidates-per-page (+ 2 zero-input-current-page)))))
+    (zero-input-debug "decide whether to fetch more candidates, has %s candidates, last fetch size=%s, new-fetch-size=%s\n" len zero-input-fetch-size new-fetch-size)
     (if (and (< len new-fetch-size)
 	     (< zero-input-fetch-size new-fetch-size))
-	(funcall zero-input-build-candidates-async-func
-		 zero-input-preedit-str
-		 new-fetch-size
-		 (lambda (candidates)
-		   (zero-input-build-candidates-complete candidates)
-		   (setq zero-input-fetch-size (max new-fetch-size
-					      (length candidates)))
-		   (zero-input-just-page-down)))
+	(progn
+	  (zero-input-debug "will fetch more candidates")
+	  (funcall zero-input-build-candidates-async-func
+		   zero-input-preedit-str
+		   new-fetch-size
+		   (lambda (candidates)
+		     (setq zero-input-candidates candidates)
+		     (setq zero-input-fetch-size (max new-fetch-size
+						      (length candidates)))
+		     (zero-input-just-page-down))))
       (zero-input-just-page-down))))
 
 (defun zero-input-handle-preedit-char-default (ch)
@@ -644,11 +649,26 @@ N is the argument passed to `self-insert-command'."
       (zero-input-debug "unexpected state: %s\n" zero-input-state)
       (self-insert-command n)))))
 
+(defun zero-input-get-initial-fetch-size ()
+  "Return initial fetch size."
+  (cond
+   ((<= zero-input-initial-fetch-size zero-input-candidates-per-page)
+    (1+ zero-input-candidates-per-page))
+   ((zerop (mod zero-input-initial-fetch-size zero-input-candidates-per-page))
+    (1+ zero-input-initial-fetch-size))
+   (t zero-input-initial-fetch-size)))
+
 (defun zero-input-preedit-str-changed ()
   "Called when preedit str is changed and not empty.  Update and show candidate list."
   (setq zero-input-fetch-size 0)
   (setq zero-input-current-page 0)
-  (funcall zero-input-build-candidates-async-func zero-input-preedit-str zero-input-initial-fetch-size 'zero-input-build-candidates-complete))
+  (let ((new-fetch-size (zero-input-get-initial-fetch-size)))
+    (funcall zero-input-build-candidates-async-func
+	     zero-input-preedit-str new-fetch-size
+	     (lambda (candidates)
+	       (setq zero-input-candidates candidates)
+	       (setq zero-input-fetch-size (max new-fetch-size (length candidates)))
+	       (zero-input-show-candidates candidates)))))
 
 (defun zero-input-backspace-default ()
   "Handle backspace key in `zero-input--state-im-preediting' state."
@@ -1162,6 +1182,9 @@ You can find the xml file locally at
 `zero-input-pinyin-service-interface-xml-url'."
   :type 'integer
   :group 'zero-input-pinyin)
+(defvar zero-input-pinyin-use-async-fetch nil
+  "Non-nil means use async dbus call to get candidates.")
+(setq zero-input-pinyin-use-async-fetch nil)
 
 (defvar-local zero-input-pinyin-state nil
   "Zero-input-pinyin internal state.  could be nil or
@@ -1208,10 +1231,14 @@ You can find the xml file locally at
   "Synchronously build candidates list.
 
 PREEDIT-STR the preedit string.
-FETCH-SIZE fetch at least this many candidates if possible."
+FETCH-SIZE fetch at least this many candidates if possible.
+
+Return candidates list"
   (zero-input-debug "zero-input-pinyin building candidate list synchronously\n")
   (let ((result (zero-input-pinyin-service-get-candidates preedit-str fetch-size)))
-    (setq zero-input-fetch-size (max fetch-size (length (cl-first result))))
+    ;; update zero-input-candidates and zero-input-fetch-size is done in async
+    ;; complete callback. This function only care about building the
+    ;; candidates and updating zero-input-pinyin specific.
     (setq zero-input-pinyin-used-preedit-str-lengths (cl-second result))
     (setq zero-input-pinyin-candidates-pinyin-indices (cl-third result))
     (cl-first result)))
@@ -1228,9 +1255,10 @@ COMPLETE-FUNC the callback function when async call completes.  it's called with
    preedit-str
    fetch-size
    (lambda (candidates matched_preedit_str_lengths candidates_pinyin_indices)
+     (setq zero-input-candidates candidates)
+     (setq zero-input-fetch-size (max fetch-size (length candidates)))
      (setq zero-input-pinyin-used-preedit-str-lengths matched_preedit_str_lengths)
      (setq zero-input-pinyin-candidates-pinyin-indices candidates_pinyin_indices)
-     (setq zero-input-fetch-size (max fetch-size (length candidates)))
      ;; Note: with dynamic binding, this command result in (void-variable
      ;; complete-func) error.
      (funcall complete-func candidates))))
@@ -1243,11 +1271,34 @@ COMPLETE-FUNC the callback function when async call completes.  it's called with
        (not (= ch ?u))
        (not (= ch ?v))))
 
+(defun zero-input-pinyin-build-candidates-unified (preedit-str fetch-size complete-func)
+  "Build candidate list, when done call complete-func on it.
+
+This may call sync or async dbus method depending on
+`zero-input-pinyin-use-async-fetch'.
+
+PREEDIT-STR the preedit string.
+FETCH-SIZE fetch at least this many candidates if possible.
+COMPLETE-FUNC the callback function when sync/async call completes.
+              it's called with fetched candidates list as parameter."
+  (if zero-input-pinyin-use-async-fetch
+      (zero-input-pinyin-build-candidates-async
+       preedit-str fetch-size complete-func)
+    (let ((candidates (zero-input-pinyin-build-candidates
+		       preedit-str fetch-size)))
+      (setq zero-input-candidates candidates)
+      (setq zero-input-fetch-size (max fetch-size (length candidates)))
+      (funcall complete-func candidates))))
+
 (defun zero-input-pinyin-pending-preedit-str-changed ()
   "Update zero states when pending preedit string changed."
   (setq zero-input-fetch-size 0)
   (setq zero-input-current-page 0)
-  (zero-input-pinyin-build-candidates-async zero-input-pinyin-pending-preedit-str zero-input-initial-fetch-size 'zero-input-build-candidates-complete))
+  (let ((fetch-size (zero-input-get-initial-fetch-size))
+	(preedit-str zero-input-pinyin-pending-preedit-str))
+    (zero-input-pinyin-build-candidates-unified
+     preedit-str fetch-size
+     #'zero-input-show-candidates)))
 
 (defun zero-input-pinyin-commit-nth-candidate (n)
   "Commit Nth candidate and return true if it exists, otherwise, return false."
@@ -1333,16 +1384,24 @@ Otherwise, just return nil."
 
 This is different from zero-input-framework because I need to support partial commit"
   (let ((len (length zero-input-candidates))
-	(new-fetch-size (* zero-input-candidates-per-page (+ 2 zero-input-current-page))))
+	(new-fetch-size (1+ (* zero-input-candidates-per-page (+ 2 zero-input-current-page)))))
+    ;; (zero-input-debug
+    ;;  "fetch more candidates? on page %s, has %s candidates, last-fetch-size=%s, new-fetch-size=%s\n"
+    ;;  zero-input-current-page len zero-input-fetch-size new-fetch-size)
     (if (and (< len new-fetch-size)
 	     (< zero-input-fetch-size new-fetch-size))
-	(let ((preedit-str (if (eq zero-input-pinyin-state zero-input-pinyin--state-im-partial-commit) zero-input-pinyin-pending-preedit-str zero-input-preedit-str)))
-	  (zero-input-pinyin-build-candidates-async
+	(let ((preedit-str (if (eq zero-input-pinyin-state
+				   zero-input-pinyin--state-im-partial-commit)
+			       zero-input-pinyin-pending-preedit-str
+			     zero-input-preedit-str)))
+	  (zero-input-debug
+	   "will fetch more candidates new-fetch-size=%s\n" new-fetch-size)
+	  (zero-input-pinyin-build-candidates-unified
 	   preedit-str
 	   new-fetch-size
-	   (lambda (candidates)
-	     (zero-input-build-candidates-complete candidates)
+	   (lambda (_candidates)
 	     (zero-input-just-page-down))))
+      (zero-input-debug "won't fetch more candidates\n")
       (zero-input-just-page-down))))
 
 (defun zero-input-pinyin-handle-preedit-char (ch)
@@ -1420,24 +1479,42 @@ DIGIT 0 means delete 10th candidate."
 ;; register IM to zero framework
 ;;===============================
 
-(zero-input-register-im
- 'pinyin
- '((:build-candidates . zero-input-pinyin-build-candidates)
-   ;; comment to use sync version, uncomment to use async version.
-   ;; (:build-candidates-async . zero-input-pinyin-build-candidates-async)
-   (:can-start-sequence . zero-input-pinyin-can-start-sequence)
-   (:handle-preedit-char . zero-input-pinyin-handle-preedit-char)
-   (:get-preedit-str-for-panel . zero-input-pinyin-get-preedit-str-for-panel)
-   (:handle-backspace . zero-input-pinyin-backspace)
-   (:init . zero-input-pinyin-init)
-   (:shutdown . zero-input-pinyin-shutdown)
-   (:preedit-start . zero-input-pinyin-preedit-start)
-   (:preedit-end . zero-input-pinyin-preedit-end)))
+(defun zero-input-pinyin-register-im ()
+  "Register pinyin input method in zero framework."
+  (zero-input-register-im
+   'pinyin
+   (append
+    (if zero-input-pinyin-use-async-fetch
+	'((:build-candidates-async . zero-input-pinyin-build-candidates-async))
+      nil)
+    '((:build-candidates . zero-input-pinyin-build-candidates)
+      (:can-start-sequence . zero-input-pinyin-can-start-sequence)
+      (:handle-preedit-char . zero-input-pinyin-handle-preedit-char)
+      (:get-preedit-str-for-panel . zero-input-pinyin-get-preedit-str-for-panel)
+      (:handle-backspace . zero-input-pinyin-backspace)
+      (:init . zero-input-pinyin-init)
+      (:shutdown . zero-input-pinyin-shutdown)
+      (:preedit-start . zero-input-pinyin-preedit-start)
+      (:preedit-end . zero-input-pinyin-preedit-end)))))
 
 ;;============
 ;; public API
 ;;============
 
+(defun zero-input-pinyin-enable-async ()
+  "Use async call to fetch candidates."
+  (interactive)
+  (setq zero-input-pinyin-use-async-fetch t)
+  (zero-input-pinyin-register-im)
+  (message "Enabled async mode"))
+
+(defun zero-input-pinyin-disable-async ()
+  "Use sync call to fetch candidates."
+  (interactive)
+  (setq zero-input-pinyin-use-async-fetch nil)
+  (zero-input-pinyin-register-im)
+  (message "Disabled async mode"))
+
 (provide 'zero-input-pinyin)